排序算法思想一选择&冒泡&快排
1 选择排序简介
选择排序(Selection Sort)是一种简单直观的排序算法。它的工作原理是:首先在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序步骤
- 在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
选择排序分析
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
2 选择排序示例
package cn.zxc.demo.leetcode_demo.base_algorithm.sort;
import java.util.Arrays;
/**
* 选择排序
* 时间复杂度:O(n^2)
* 空间复杂度:O(1)
* 稳定性:不稳定
* 实现步骤:
* 1.从第一个元素开始,该元素可以认为已经被排序
* 2.在未排序序列中找到最小元素,将其与未排序序列的第一个元素交换,使已排序序列的元素个数增加1
* 3.重复第二步,直到所有元素均排序完毕
*/
public class SelectionSort {
public static void main(String[] args) {
int[] nums = {1, 3, 2, 5, 4};
SelectionSort selectionSort = new SelectionSort();
selectionSort.selectionSort(nums);
System.out.println(Arrays.toString(nums));
selectionSort.selectionSort2(nums);
System.out.println(Arrays.toString(nums));
}
/**
* 选择排序:升序
* @param nums
* @return
*/
public int[] selectionSort(int[] nums)
{
for (int i = 0; i < nums.length; i++) {
int temp = nums[i];
int index = i;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < temp){
temp = nums[j];
index = j;
}
}
nums[index] = nums[i];
nums[i] = temp;
}
return nums;
}
public int[] selectionSort2(int[] nums)
{
for (int i = 0; i < nums.length; i++) {
int max = nums[i];
int index = i;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] > max){
max = nums[j];
index = j;
}
}
nums[index] = nums[i];
nums[i] = max;
}
return nums;
}
}
3 冒泡排序简介
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
冒泡排序步骤
- 比较相邻的元素:如果第一个比第二个大(升序排序),就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
示例
假设有一个数组 arr = [64, 34, 25, 12, 22, 11, 90]
,以下是冒泡排序的过程:
- 第一轮比较和交换后:
[34, 25, 12, 22, 11, 64, 90]
- 第二轮比较和交换后:
[25, 12, 22, 11, 34, 64, 90]
- 第三轮比较和交换后:
[12, 22, 11, 25, 34, 64, 90]
- 第四轮比较和交换后:
[11, 12, 22, 25, 34, 64, 90]
- 第五轮比较和交换后(因为 12 已经在 11 的右边,所以不需要交换):
[11, 12, 22, 25, 34, 64, 90]
冒泡排序分析
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
优化
-
如果在某一轮遍历中没有发生任何交换,那么数组已经排序完成,可以提前结束算法。
/** * 冒泡排序:升序优化 * @param nums */ public int[] bubbleSort3(int[] nums) { for (int i = 0; i < nums.length; i++) { boolean flag = false; // 优化 for (int j = i + 1; j < nums.length; j++) { if (nums[j] < nums[i]){ int temp = nums[j]; nums[j] = nums[i]; nums[i] = temp; flag = true; } } if (!flag) break; } return nums; }
-
冒泡排序可以从两端向中间进行,这种优化称为“鸡尾酒排序”或“双向冒泡排序”。
4 冒泡排序示例
/**
* 冒泡排序
* 时间复杂度O(n^2)
* 空间复杂度O(1)
*/
public class BubbleSorting {
public static void main(String[] args) {
BubbleSorting bubbleSorting = new BubbleSorting();
int[] nums = {2, 2, 7, 4, 11, 6, 17, 8, 9, 10};
int[] bubbleSort1 = bubbleSorting.bubbleSort(nums);
bubbleSorting.print(bubbleSort1);
System.out.println();
int[] bubbleSort2 = bubbleSorting.bubbleSort2(nums);
bubbleSorting.print(bubbleSort2);
System.out.println();
int[] bubbleSort3 = bubbleSorting.bubbleSort3(nums);
bubbleSorting.print(bubbleSort3);
}
/**
* 冒泡排序:降序
* @param nums
*/
public int[] bubbleSort(int[] nums) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] > nums[i]){
int num = nums[j];
nums[j] = nums[i];
nums[i] = num;
}
}
}
return nums;
}
/**
* 冒泡排序:升序
* @param nums
*/
public int[] bubbleSort2(int[] nums) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[i]){
int temp = nums[j];
nums[j] = nums[i];
nums[i] = temp;
}
}
}
return nums;
}
/**
* 冒泡排序:升序优化
* @param nums
*/
public int[] bubbleSort3(int[] nums) {
for (int i = 0; i < nums.length; i++) {
boolean flag = false;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[i]){
int temp = nums[j];
nums[j] = nums[i];
nums[i] = temp;
flag = true;
}
}
if (!flag) break;
}
return nums;
}
public void print(int[] nums) {
for (int i = 0; i < nums.length; i++) {
System.out.print(nums[i] + " ");
}
}
}
5 快速排序简介
快速排序是什么
快速排序(Quicksort)是一种高效的排序算法,它采用了分治(Divide and Conquer)的思想。
- 分治策略:快速排序通过选择一个基准元素(pivot),将待排序的序列分成两部分,使得左边部分的所有元素都小于或等于基准元素,右边部分的所有元素都大于或等于基准元素。
- 递归处理:然后,对左右两部分分别递归地应用上述过程,直到整个序列有序。
快速排序步骤
- 选择基准元素:通常选择序列的第一个元素作为基准元素,但也可以采用其他策略,如随机选择或三者取中等。
- 分区操作:通过一趟排序将待排序列分成两部分,使得左边部分的所有元素都小于或等于基准元素,右边部分的所有元素都大于或等于基准元素。
- 递归排序:对左右两部分分别递归地应用快速排序算法。
快速排序分析
时间复杂度:O(nlogn)
- 最坏情况:当每次分区操作都出现最不平衡的情况时,即每次分区后一边序列为空,另一边序列只比原序列少一个元素时,快速排序的时间复杂度为O(n^2)。
- 最好情况:当每次分区操作都能将序列平均分成两部分时,快速排序的时间复杂度为O(nlogn)。
- 平均情况:快速排序的平均时间复杂度也为O(nlogn)。
空间复杂度:O(n)
稳定性:不稳定
基准关键字的选取
基准关键字的选取对快速排序的性能有很大影响。常用的选取方式有:
- 固定选取:直接选择序列的第一个或最后一个元素作为基准关键字。
- 三者取中:选择序列首、尾和中间位置上的三个元素,取其中值作为基准关键字。
- 随机选取:在序列的left和right之间随机选择一个元素作为基准关键字。
课外阅读:图解快速排序[ https://blog.csdn.net/justidle/article/details/104203963 ]
6 快速排序示例
package cn.zxc.demo.leetcode_demo.base_algorithm.sort;
import java.util.Arrays;
/**
* 快速排序
* 时间复杂度:O(nlogn)
* 空间复杂度:O(logn)
* 稳定性:不稳定
* 实现步骤
* 1.选择一个基准值,一般选择第一个元素或者最后一个元素
* 2.将数组分为两个部分,左边比基准值小,右边比基准值大
* 3.递归调用
*/
public class QuickSort {
public static void main(String[] args)
{
int[] nums = {1,3,2,45,5,17,7,8,19,10};
QuickSort quickSort = new QuickSort();
quickSort.quickSort(nums, 0, nums.length - 1);
System.out.println(Arrays.toString(nums));
}
public void quickSort(int[] nums, int left, int right)
{
int base = nums[left];
int l_point = left;
int r_point = right;
while (l_point < r_point){
// 找到右边比base小的值
while (l_point < r_point && nums[r_point] >= base){
r_point--;
}
if (l_point < r_point){
int temp = nums[r_point];
nums[r_point] = nums[l_point];
nums[left] = temp;
}
// 找到左边比base大的值
while (l_point < r_point && nums[l_point] <= base){
l_point++;
}
if (l_point < r_point){
int temp = nums[r_point];
nums[r_point] = nums[l_point];
nums[l_point] = temp;
}
}
// 递归
if (l_point > left){
quickSort(nums,left,l_point-1);
}
if (right > l_point){
quickSort(nums,l_point+1,right);
}
}
}
7 各种排序对比
排序名称 | 时间复杂度 | 是否稳定 | 额外空间开销 |
---|---|---|---|
插入排序 | O(n^2) | 稳定 | O(1) |
冒泡排序 | O(n^2) | 稳定 | O(1) |
选择排序 | O(n^2) | 不稳定 | O(1) |
希尔排序 | O(n^2) | 不稳定 | O(1) |
归并排序 | O(nlogn) | 稳定 | O(n) |
快速排序 | O(nlogn) | 不稳定 | O(1) |
如何选择排序算法?
在选择上述的排序算法时,通常需要考虑以下几个指标:
- 时间复杂度:算法执行所需的时间。这通常通过最好情况、平均情况和最坏情况的时间复杂度来衡量。
- 插入排序、选择排序、冒泡排序的时间复杂度通常是 O(n^2)。
- 希尔排序的时间复杂度依赖于间隔序列的选择,但通常比 O(n^2) 要好。
- 归并排序和快速排序的时间复杂度在平均和最好情况下是 O(n log n),但在最坏情况下,快速排序可能会退化为 O(n^2)(当输入数组已经有序或接近有序时)。
- 归并排序的最坏情况时间复杂度总是 O(n log n)。
- 空间复杂度:算法执行所需的额外空间。
- 插入排序、选择排序、冒泡排序和希尔排序是原地排序算法(in-place sorting),即它们只需要 O(1) 的额外空间(除了输入数组本身)。
- 归并排序不是原地排序算法,因为它需要额外的空间来合并两个已排序的子数组。其空间复杂度是 O(n)。
- 快速排序在递归调用中也需要额外的栈空间,但在平均情况下,其空间复杂度是 O(log n)。但在最坏情况下,如果递归调用不平衡,它可能需要 O(n) 的空间。
- 稳定性:如果相等的元素在排序后保持其原始顺序,则算法是稳定的。
- 插入排序、冒泡排序、归并排序和希尔排序(当间隔为1时)是稳定的。
- 选择排序、快速排序和希尔排序(当间隔大于1时)通常不是稳定的。
- 数据的特性:
- 如果输入数据已经部分有序或接近有序,快速排序可能会表现得较差,因为它在最坏情况下的时间复杂度是 O(n^2)。在这种情况下,插入排序或希尔排序可能是一个更好的选择。
- 如果内存限制是一个问题,并且需要原地排序,那么插入排序、选择排序、冒泡排序或希尔排序可能是更好的选择。
- 如果需要稳定的排序算法,那么应该选择插入排序、冒泡排序、归并排序或希尔排序(当间隔为1时)。
- 实现细节:不同的排序算法可能在不同的编程语言或环境中具有不同的性能特性。因此,了解特定实现的性能特点也是很重要的。
- 并行性和分布式处理:一些排序算法(如归并排序和快速排序)可以更容易地并行化或分布到多个处理器上,以进一步提高性能。
8 示例题
如何对一个省200万学生的高考成绩(假设成绩最多只有2位小数,0~900范围)进行排序,用尽可能高效的算法。
import cn.hutool.core.io.FileUtil;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
/**
* 如何对一个省200万学生的高考成绩(假设成绩最多只有2位小数,0~900范围)进行排序,用尽可能高效的算法。
* 1、使用快速排序实现
* 2、使用归并排序实现
* 3、直接使用数组特性解决
*/
public class ExampleDemo {
public static void main(String[] args) throws IOException {
double[] nums = iniArray(2000000);
// long start = System.currentTimeMillis();
// 快速排序
// quickSort(nums, 0, nums.length - 1);
// 归并排序
// mergeSort(nums, 0, nums.length - 1);
// System.out.println(" 耗时: " + (System.currentTimeMillis() - start) + "ms");
// 将结果进行输出
// out2File(nums);
// 直接使用数组特性解决
arrayHandle(nums);
}
private static double[] iniArray(int i) {
double[] nums = new double[i];
// 保留两位数
for (int j = 0; j < i; j++) {
nums[j] = Math.random() * 900;
nums[j] = Math.round(nums[j] * 100) / 100.0;
}
return nums;
}
private static void arrayHandle(double[] nums) throws IOException {
long start = System.currentTimeMillis();
// 1、初始化一个长度为 900 * 100 的数组
int[] array = new int[100000];
// 2、遍历nums数组,对nums数组中的元素 * 100,并以值为array的下标,值+1
for (double num : nums) {
array[(int) (num * 100)]++;
}
System.out.println(" 耗时: " + (System.currentTimeMillis() - start) + "ms");
BufferedWriter writer = FileUtil.getWriter(new File("res.txt"), "UTF-8", true);
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i]; j++) {
nums[i] = i / 100.0;
writer.write(nums[i] + "");
writer.newLine();
}
}
writer.flush();
writer.close();
}
private static void out2File(double[] nums) throws IOException {
BufferedWriter writer = FileUtil.getWriter(new File("res.txt"), "UTF-8", true);
for (int i = 0; i < nums.length; i++) {
writer.write(nums[i] + "");
writer.newLine();
}
writer.flush();
writer.close();
}
public static void quickSort(double[] nums,int left, int right){
double base = nums[left];
int l_point = left;
int r_point = right;
while (l_point < r_point){
// 找到右边有所有小于base的元素
while (l_point < r_point && nums[r_point] >= base){
r_point--;
}
if (l_point < r_point){
double temp = nums[r_point];
nums[r_point] = nums[l_point];
nums[l_point] = temp;
}
// 找到左边所有小于base的元素
while (l_point < r_point && nums[l_point] <= base){
l_point++;
}
if (l_point < r_point){
double temp = nums[l_point];
nums[l_point] = nums[r_point];
nums[r_point] = temp;
}
}
if (l_point > left)
quickSort(nums, left, l_point - 1);
if (r_point < right)
quickSort(nums, r_point + 1, right);
}
public static void mergeSort(double[] nums,int left,int right){
if (left >= right)
return;
int mid = (left + right) / 2;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
// 进行合并
double[] temp = new double[right - left + 1];
int l_point = left;
int r_point = mid + 1;
while (l_point <= mid && r_point <= left){
if (nums[l_point] <= nums[r_point]){
temp[l_point - left] = nums[l_point];
l_point++;
}else {
temp[l_point - left] = nums[r_point];
r_point++;
}
}
while (l_point <= mid){
temp[l_point - left] = nums[l_point];
l_point++;
}
while (r_point <= right){
temp[r_point - left] = nums[r_point];
r_point++;
}
for (int i = 0; i < temp.length; i++) {
nums[i + left] = temp[i];
}
}
}