整理一下常见的排序算法。
1、插入排序
插入排序是基础的排序之一,插入排序的过程,脑补打扑克,分成两部分:一部分是手里的牌(已经排好序),一部分是要拿的牌(无序)。这种往一个有序的集合里面插入元素,插入后序列仍然有序这就是插入排序算法思路。
public static void main(String[] args) { int[] a = {3, 43, 32, 2, 4, 6, 23, 9, 8, 2, 6, 7, 43}; int n = a.length; // 无序的部分,从第二个开始,因为第一个是有序的 for (int i = 1; i < n; i++) { int data = a[i]; int j = i - 1; // 有序的部分,从后往前挨个遍历 for (; j >= 0; j--) { // 如果当前元素大于data,表示data要往前移 if (a[j] > data) { a[j + 1] = a[j]; } else { break; } } a[j + 1] = data; } System.out.println(Arrays.toString(a)); }
输出结果:[2, 2, 3, 4, 6, 6, 7, 8, 9, 23, 32, 43, 43]
插入排序的时间复杂度是O(n^2),是稳定的,它是最基础的排序算法。如果对插入排序进行优化,我们从上面可以看到就是break的地方越多越好,就表示前面都是有序的,且当前比较元素已经找到了插入的位置。
2、希尔排序
希尔排序是插入排序的改良版。把数据下标按照一定的增量分组,对每组使用插入排序,当增量减至1时,排序终止。 其改良的地方尽可能分出来更多的有序段,因为从插入排序可知其对有序段的处理速度是很快的。
public static void main(String[] args) { int[] a = {3, 43, 32, 2, 4, 6, 23, 9, 8, 2, 6, 7, 43}; int n = a.length; // 分组,每次下标按照gap递增 for (int gap = n / 2; gap >= 1; gap /= 2) { // 下面的处理就和插入排序一样了 for (int i = 1; i < n; i+=gap) { int data = a[i]; int j = i - gap; for (; j >= 0; j-=gap) { if (a[j] > data) { a[j + gap] = a[j]; } else { break; } } a[j + gap] = data; } } System.out.println(Arrays.toString(a)); }
希尔排序能一定程度的提高插入排序的性能,时间复杂度为O(n^2),但是这样分组,中间还会有交换,排序是不稳定的。
3、归并排序
归并排序是jdk源码中使用的排序,时间复杂度为O(nlogn),是非常高效的一种排序算法,原理有点类似于二分,分到最后一个,就是有序的,然后再进行合并,写这个代码要用到递归的思想。
public class MergeSort { static int[] a = {3, 43, 32, 2, 4, 6, 23, 9, 8, 2, 6, 7, 43}; // 借用数组临时保存数据 static int[] temp = new int[a.length]; public static void main(String[] args) { mergeSort(a, 0, a.length - 1); System.out.println(Arrays.toString(a)); } // 归并排序的核心在于先分后合,数据拆到只剩一个就是有序的,再进行合并 public static void mergeSort(int[] a, int left, int right) { // 终止条件,只有一个数,就不用再分了 if (left < right) { int mid = (left + right) / 2; mergeSort(a, left, mid); mergeSort(a, mid + 1, right); merge(a, left, mid, right); } } // left-mid之间是有序的,mid-right之间是有序的,合并两个有序的数组 public static void merge(int[] a, int left, int mid, int right) { // loc下标用来指向临时数组赋值的位置 int loc = left; // 用来标记左边数组合并到哪个位置 int p1 = left; // 用来标记用边数组合并到哪个位置 int p2 = mid + 1; // 循环条件是左边和右边都没合并完成 while (p1 <= mid && p2 <= right) { // 现在要做的就是左边和右边比较和交换 if (a[p1] <= a[p2]) { temp[loc++] = a[p1++]; } else { temp[loc++] = a[p2++]; } } // 上面的循环终止了,但是不知道是左边的还是右边的合并完了,需要处理未合并的数据 while (p1 <= mid) { temp[loc++] = a[p1++]; } while (p2 <= right) { temp[loc++] = a[p2++]; } // 最后,将临时数组中合并完的有序数据存入原数组 for (int i = left; i <= right; i++) { a[i] = temp[i]; } } }
时间复杂度o(nlogn),是稳定的
4、选择排序
选择排序和插入排序很类似,也分有序和无序两部分,不同的思想是插入排序对数据的操作是移动,选择排序是交换,每次都能找到最小的数。
public static void main(String[] args) { int[] a = {3, 43, 32, 2, 4, 6, 23, 9, 8, 2, 6, 7, 43}; int n = a.length; // 从第一个开始遍历到倒数第二个 for (int i = 0; i < n - 1; i++) { // 一次找到一个最小的值,记录下标 for (int j = i + 1; j < n; j++) { int index = i; if (a[index] > a[j]) { index = j; } // 将a[i]和最小值交换 a[i] = (a[i] + a[index]) - (a[index] = a[i]); } } System.out.println(Arrays.toString(a)); }
时间复杂度O(n^2),不稳定
5、冒泡排序
每次冒泡操作都会对相邻的两个元素进行比较,不满足大小关系就交换,一次冒泡能让一个元素移动到它应该在的位置,n次冒泡排序完成。
public static void main(String[] args) { int[] a = {3, 43, 32, 2, 4, 6, 23, 9, 8, 2, 6, 7, 43}; int n = a.length; for (int i = 0; i < n - 1; i++) { for (int j = i + 1; j < n; j++) { if (a[i] > a[j]) { a[i] = (a[i] + a[j]) - (a[j] = a[i]); } } } System.out.println(Arrays.toString(a)); }
冒泡的排序时间复杂度是O(n^2),交换和比较的次数是比较多的,是稳定的
6、快速排序
快排的思路是找一个基准数,从后往前找比之小的交换,从前往后找比之大的交换,这样循环处理之后比它小的都在左边,比它大的都在右边,对左右分别递归,完成排序。
public static void qSort(int[] a, int left, int right) { // 定义基准数为第一个数 int base = a[left]; // 左指针,从左往右找比之大的数 int p1 = left; // 右指针,从右往左找比之小的数 int p2 = right; // 终止条件,已经找到了同一个位置 while (p1 < p2) { // 左右指针还未指向同一个位置且基准数比右边的小 while (p1 < p2 && base <= a[p2]) { p2--; } // 左右指针仍未指向同一个位置,说明上面循环退出的条件是:从右到左找到了比base小的数,进行交换 if (p1 < p2) { a[p1] = (a[p1] + a[p2]) - (a[p2] = a[p1]); p1++; } // 和上面类似,这边是从左向右找比基准数大的数进行交换 while (p1 < p2 && base >= a[p1]) { p1++; } if (p1 < p2) { a[p1] = (a[p1] + a[p2]) - (a[p2] = a[p1]); p2--; } } // 左半部分递归 if (left < p1) { qSort(a, left, p1 - 1); } // 右半部分递归 if (p2 < right) { qSort(a, p2 + 1, right); } }
快排的时间复杂度是O(nlogn),最坏的情况是O(n^2)。优化快排性能主要从优化基准数方向考虑,比如取三个数计算出合适的基准数。
快排和归并有些相似处,都会对数据进行拆分,不同的是归并排序从上到下处理,先处理子问题,然后再合并。而快排就是从上到下分区再处理子问题,不用合并,类似于尾递归。
这么多排序算法,如何选择,首先要看场景,需不需要稳定排序,然后看数据量,小的话直接选插入也没什么问题(虽然它是O(n^2)),然后还要分析空间,归并排序就需要额外开辟部分空间。所有没有说一定适用的排序算法,视情况而定,如果不好分析,选归并或者快排,基本能解决问题。