各个排序算法的时间复杂度及稳定性
排序方法 |
时间复杂度(最坏) |
时间复杂度(最好) |
时间复杂度(平均) |
空间复杂度 |
排序方式 |
稳定性 |
冒泡排序 |
O(n^2^) |
O(n) |
O(n^2^) |
O(1) |
in-place |
稳定 |
选择排序 |
O(n^2^) |
O(n^2^) |
O(n^2^) |
O(1) |
in-place |
不稳定 |
插入排序 |
O(n^2^) |
O(n^2^) |
O(n) |
O(1) |
in-place |
稳定 |
希尔排序 |
O(n^2^) |
O(n) |
O(n^1.3^) |
O(1) |
in-place |
不稳定 |
快速排序 |
O(n^2^) |
O(nlog n) |
O(nlog n) |
O(log n) |
in-place |
不稳定 |
归并排序 |
O(nlog n) |
O(nlog n) |
O(nlog n) |
O(n) |
out-place |
稳定 |
堆排序 |
O(nlog n) |
O(nlog n) |
O(nlog n) |
O(1) |
in-place |
不稳定 |
计数排序 |
O(n + k) |
O(n + k) |
O(n + k) |
O(k) |
out-place |
稳定 |
桶排序 |
O(n^2^) |
O(n + k) |
O(n + k) |
O(n + k) |
out-place |
稳定 |
基数排序 |
O(n * k) |
O(n * k) |
O(n * k) |
O(n + k) |
out-place |
稳定 |
冒泡排序
- 冒泡排序作为最基础的排序,博主大学各项期末考试就被考过至少三次,在这里给出优化版写法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class Sort { public void BubbleSort(int[] arr) { for (int i = 1; i < arr.length; i++) { boolean flag = true; for (int j = 0; j < arr.length - i; j++) { if (arr[j] > arr[j + 1]) { swap(arr, j, j + 1); flag = false; } } if (flag) { break; } } }
private static void swap(int[] arr, int i, int j) { arr[i] = arr[i] ^ arr[j]; arr[j] = arr[i] ^ arr[j]; arr[i] = arr[i] ^ arr[j]; } }
|
选择排序
1.不断在未排序序列中找到最小的元素,与本轮循环起始元素比较。
2.若此元素较小,则更新最小元素下标。
3.交换本轮起始位置元素及最小位置元素。
4.重复上述步骤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Sort { public void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) { int minIndex = i; for (int j = arr.length - 1; j > i; j--) { if (arr[j] < arr[minIndex]) { minIndex = j; } } if (i != minIndex) { swap(arr, i, minIndex); } } }
private static void swap(int[] arr, int i, int j) { arr[i] = arr[i] ^ arr[j]; arr[j] = arr[i] ^ arr[j]; arr[i] = arr[i] ^ arr[j]; } }
|
插入排序
1.把第一个元素看作已有序。
2.从第二个元素开始依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Sort { public void insertSort(int[] arr) { for (int i = 1; i < arr.length; i++) { int tmp = arr[i]; int j = i - 1; while (j >= 0 && arr[j] > tmp) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = tmp; } } }
|
希尔排序
- 希尔排序是对插入排序的优化,在元素无序且step > 1时,分组并交换能让无序序列更快趋于有序,此时就可以发挥序列趋于有序时插入排序更快的优势。
- 但需要注意的是,如果数组本就有序或趋于有序,插入排序要优许希尔排序,因为它省去了分组的过程,直接对数组进行排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Sort { public void shellSort(int[] arr) { for (int step = arr.length / 2; step > 0; step /= 2) { for (int i = step; i < arr.length; i += step) { int tmp = arr[i]; int j = i - step; while (j > 0 && arr[j] > tmp) { arr[j + step] = arr[j]; j -= step; } arr[j + step] = tmp; } } } }
|
快速排序
- 快排,顾名思义就是“快”,就连JDK自带的
Arrays.sort()
在传入整形参数时也会调用其自身的双轴快速排序,可见其重要性。
- 快排体现了“分治”的思想,它以基准值为根节点将一个数组转化为二叉树进行深度优先查找,并依次将符合条件的元素放入对应的位置。
- 要想找到基准,最关键的莫过于
partition()
方法,再构造这个方法前,我们先搭起快排的框架。1 2 3 4 5 6
| public void quickSort(int[] arr, int L, int R){ if (L >= R) return; int pivot = partition(arr, L, R); quickSort(arr, L, pivot - 1); quickSort(arr, pivot + 1, R); }
|
- 如果遇上了数组量巨大且有序的情况,插入排序看了直接笑嘻,但这是快排的最坏情况,因为若不做处理,二叉树会退化为一条单链表,最后出现
StackOverFlowError()
,要解决栈溢出问题,我们要先引入三数取中法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| private static int MiddleOfThree(int[] arr, int L, int R) { int M = L + ((R - L) >> 1); if (arr[L] < arr[R]) { if (arr[M] < arr[L]) { return L; } else if (arr[M] > arr[R]) { return R; } else { return M; } } else { if (arr[M] > arr[L]) { return L; } else if (arr[M] < arr[R]) { return R; } else { return M; } } }
|
- 这样做的好处是:第一次返回的基准值
pivot
能保证是一个“不大不小”的元素,这样就算遇到了最差情况,它仍然会演化成一颗二叉树。
- 下面正式介绍
partition()
方法
分割方法
Hoare法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
private static int partition(int[] arr, int L, int R) { int pivotVal = arr[L]; int pivotIdx = L; while (L < R) { while (L < R && arr[R] >= pivotVal) { R--; } while (L < R && arr[L] <= pivotVal) { L++; } swap(arr, L, R); } swap(arr, L, pivotIdx); return L; }
|
- 初始基准值为arr[L],就要从右边开始找,否则可能出现最后基准值交换后又换回来的情况。
- >=和<=不可改为>和<,若左右元素相等,就进入了死循环。
挖坑法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
private static int partition(int[] arr, int L, int R) { int pivotVal = arr[L]; while (L < R) { while (L < R && arr[R] >= pivotVal) { R--; } arr[L] = arr[R]; while (L < R && arr[L] <= pivotVal) { L++; } arr[R] = arr[L]; } arr[L] = pivotVal; return L; }
|
快慢指针法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
private static int partitionPoint(int[] arr, int L, int R) { int prev = L; int cur = L + 1; while (cur < R) { while (arr[cur] < arr[L] && arr[++prev] != arr[cur]) { swap(arr, prev, cur); } cur++; } swap(arr, prev, L); return prev; }
|
迭代版
- 下面我们借助栈来实现快排的迭代版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class Sort { public void quickSortItr(int[] arr, int L, int R) { Stack<Integer> stack = new Stack<>(); int M = MiddleOfThree(arr, L, R); swap(arr, L, M); int pivot = partition(arr, L, R);
if (L + 1 < pivot) { stack.push(L); stack.push(pivot - 1); } if (R - 1 > pivot) { stack.push(pivot + 1); stack.push(R); } while (!stack.isEmpty()) { R = stack.pop(); L = stack.pop(); M = MiddleOfThree(arr, L, R); swap(arr, L, M); pivot = partition(arr, L, R); if (L + 1 < pivot) { stack.push(L); stack.push(pivot - 1); } if (R - 1 > pivot) { stack.push(pivot + 1); stack.push(R); } } } }
|
升级版
- 我们不难发现,上述的方法都是以单个下标作为基准值,下面我借用荷兰国旗问题给出快排的优化版本。它引入两个基准值下标,将数组分为小于区、等于区、大于区三个区域,这样我们就能一次排好一批数。
此算法理论上的最差时间复杂度也为*O(nlog n)*,每次递归它都随机从数组长度内取出一个值与小于区的边界值做交换,这样就使得人为无法造出最差情况,具体的数学推导不在这里展开分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class Sort { public void quickSortPlus(int[] arr, int L, int R) { if (L < R) { int randomFlag = L + (int) (Math.random() * (R - L + 1)); swap(arr, L, randomFlag); int[] pivot = partitionPlus(arr, L, R); quickSortPlus(arr, L, pivot[0] - 1); quickSortPlus(arr, pivot[1] + 1, R); } }
private static int[] partitionPlus(int[] arr, int L, int R) { int less = L; int more = R; while (L < more + 1) { if (arr[L] < arr[R]) { swap(arr, less++, L++); } else if (arr[L] > arr[R]) { swap(arr, L, more--); } else { L++; } } return new int[]{less, more}; }
private static void swap(int[] arr, int l, int r) { int tmp = arr[l]; arr[l] = arr[r]; arr[r] = tmp; } }
|
归并排序
- 归并排序是一种典型的空间换时间的排序算法,它开辟了一个与原数组等大小的辅助数组用来存储每次分治的结果,这就使得它的空间复杂度来到了O(n),它也是本文比较排序算法中唯一一个需要计算机额外开辟内存(out-place)的排序算法。
mergeSort()
体现了分的过程,而merge()
则体现了治的过程。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class Sort { public void mergeSort(int[] arr, int L, int R) { if (L >= R) return; int M = L + ((R - L) >> 1); mergeSort(arr, L, M); mergeSort(arr, M + 1, R); merge(arr, L, M, R); }
private static void merge(int[] arr, int L, int M, int R) { int[] help = new int[R - L + 1]; int i = 0; int p1 = L, p2 = M + 1; while (p1 <= M && p2 <= R) { help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; } while (p1 <= M) { help[i++] = arr[p1++]; } while (p2 <= R) { help[i++] = arr[p2++]; } for (i = 0; i < help.length; i++) { arr[L + i] = help[i]; } } }
|
迭代版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Sort { public void mergeSortItr(int[] arr) { int step = 1; while (step < arr.length) { for (int i = 0; i < arr.length; i += step * 2) { int L = i; int M = i + step - 1; int R = M + step; if (M >= arr.length) { M = arr.length - 1; } if (R >= arr.length) { R = arr.length - 1; } merge(arr, L, M, R); } step *= 2; } } }
|
堆排序
- 堆排序本质上也是利用二叉树构建了大顶堆或小顶堆,通过不断的
heapify()
过程实现。
- JDK中自带的小顶堆名为
PriorityQueue
,可以使用它解决topK问题,我们可以实现其内置的Comparator()
接口来使用自定义排序。
- 下面我们使用手动构建大顶堆的方式实现数组从小到大的排序。
构建大顶堆
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| private static void buildMaxHeap(int[] arr, int len) { for (int i = len - 1; i >= 0; i--) { heapify(arr, i, len); } }
private static void heapify(int[] arr, int p, int len) { int L = p * 2 + 1; int R = p * 2 + 2; int maxIndex = p; if (L < len && arr[L] > arr[maxIndex]) { maxIndex = L; } if (R < len && arr[R] > arr[maxIndex]) { maxIndex = R; } if (maxIndex != p) { swap(arr, maxIndex, p); heapify(arr, maxIndex, len); } }
private static void swap(int[] arr, int L, int R) { arr[L] = arr[L] ^ arr[R]; arr[R] = arr[L] ^ arr[R]; arr[L] = arr[L] ^ arr[R]; }
|
实现堆排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Sort { public void heapSort(int[] arr) { int len = arr.length; buildMaxHeap(arr, len);
for (int i = len - 1; i >= 1; i--) { swap(arr, i, 0); len--; heapify(arr, 0, len); } } }
|
计数排序
- 从这里开始的三种排序算法,都不是基于比较的排序算法,计数排序在输入的元素在n个0到k之间的整数时,其效率优于任何比较排序算法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class Sort { public void countingSort(int[] arr) { int[] maximum = getMaximum(arr); int[] help = new int[maximum[1] - maximum[0] + 1]; for (int i = 0; i < arr.length; i++) { help[arr[i] - maximum[0]]++; } int index = 0; for (int i = 0; i < help.length; i++) { while (help[i] > 0) { int val = i + maximum[0]; arr[index++] = val; help[i]--; } } }
private int[] getMaximum(int[] arr) { int min = Integer.MAX_VALUE; int max = Integer.MIN_VALUE; for (int i = 0; i < arr.length; i++) { if (arr[i] < min) { min = arr[i]; } if (arr[i] > max) { max = arr[i]; } } return new int[]{min, max}; } }
|
桶排序
基数排序