7.排序算法
3.7 排序算法
概述
比较排序算法
| 算法 | 最好 | 最坏 | 平均 | 空间 | 稳定 | 思想 | 注意事项 |
|---|---|---|---|---|---|---|---|
| 冒泡 | O(n) | O(n^2) | O(n^2) | O(1) | Y | 比较 | 最好情况需要额外判断 |
| 选择 | O(n^2) | O(n^2) | O(n^2) | O(1) | N | 比较 | 交换次数一般少于冒泡 |
| 堆 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | N | 选择 | 堆排序的辅助性较强,理解前先理解堆的数据结 构 |
| 插入 | O(n) | O(n^2) | O(n^2) | O(1) | Y | 比较 | 插入排序对于近乎有序的数据处理速度比较快,复杂度有所下降,可以提前结束 |
| 希尔 | O(nlogn) | O(n^2) | O(nlogn) | O(1) | N | 插入 | gap序列的构造有多种方式,不同方式处理的数据复杂度可能不同 |
| 归并 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | Y | 分治 | 需要额外的O(n)的存储空间 |
| 快速 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | N | 分治 | 快排可能存在最坏情况,需要 把枢轴值选取得尽量随机化来缓解最坏情况下的时间复杂度 |
非比较排序算法
| 非比较排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|
| 计数排序 | O(n+k) | O(n+k) | 稳定 |
| 桶排序 | O(n+k) | O(n+k) | 稳定 |
| 基数排序 | O(d*(n+k)) | O(n+k) | 稳定 |
其中
- n 是数组长度
- k 是桶长度
- d 是基数位数
稳定 vs 不稳定

Java 中的排序
Arrays.sort
JDK 7~13 中的排序实现
| 排序目标 | 条件 | 采用算法 |
|---|---|---|
| int[] long[] float[] double[] | size < 47 | 混合插入排序 (pair) |
| size < 286 | 双基准点快排 | |
| 有序度低 | 双基准点快排 | |
| 有序度高 | 归并排序 | |
| byte[] | size <= 29 | 插入排序 |
| size > 29 | 计数排序 | |
| char[] short[] | size < 47 | 插入排序 |
| size < 286 | 双基准点快排 | |
| 有序度低 | 双基准点快排 | |
| 有序度高 | 归并排序 | |
| size > 3200 | 计数排序 | |
| Object[] | -Djava.util.Arrays.useLegacyMergeSort=true | 传统归并排序 |
| TimSort |
JDK 14~20 中的排序实现
| 排序目标 | 条件 | 采用算法 |
|---|---|---|
| int[] long[] float[] double[] | size < 44 并位于最左侧 | 插入排序 |
| size < 65 并不是最左侧 | 混合插入排序 (pin) | |
| 有序度低 | 双基准点快排 | |
| 递归次数超过 384 | 堆排序 | |
| 对于整个数组或非最左侧 size > 4096,有序度高 | 归并排序 | |
| byte[] | size <= 64 | 插入排序 |
| size > 64 | 计数排序 | |
| char[] short[] | size < 44 | 插入排序 |
| 再大 | 双基准点快排 | |
| 递归次数超过 384 | 计数排序 | |
| size > 1750 | 计数排序 | |
| Object[] | -Djava.util.Arrays.useLegacyMergeSort=true | 传统归并排序 |
| TimSort |
- 其中 TimSort 是用归并+二分插入排序的混合排序算法
- 值得注意的是从 JDK 8 开始支持 Arrays.parallelSort 并行排序
- 根据最新的提交记录来看 JDK 21 可能会引入基数排序等优化
外部排序
1) 冒泡排序
要点
- 每轮冒泡不断地比较相邻的两个元素,如果它们是逆序的,则交换它们的位置
- 下一轮冒泡,可以调整未排序的右边界,减少不必要比较
以数组 3、2、1 的冒泡排序为例,第一轮冒泡
第二轮冒泡
未排序区域内就剩一个元素,结束
优化手段:每次循环时,若能确定更合适的右边界,则可以减少冒泡轮数
以数组 3、2、1、4、5 为例,第一轮结束后记录的 x,即为右边界

非递归版代码
public class BubbleSort {
private static void bubble(int[] a) {
int j = a.length - 1;
while (true) {
int x = 0;
for (int i = 0; i < j; i++) {
if (a[i] > a[i + 1]) {
int t = a[i];
a[i] = a[i + 1];
a[i + 1] = t;
x = i;
}
}
j = x;
if (j == 0) {
break;
}
}
}
public static void main(String[] args) {
int[] a = {6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(a));
bubble(a);
System.out.println(Arrays.toString(a));
}
}
2) 选择排序
要点
- 每一轮选择,找出最大(最小)的元素,并把它交换到合适的位置
以下面的数组选择最大值为例
非递归实现
public class SelectionSort {
public static void sort(int[] a) {
// 1. 选择轮数 a.length - 1
// 2. 交换的索引位置(right) 初始 a.length - 1, 每次递减
for (int right = a.length - 1; right > 0 ; right--) {
int max = right;
for (int i = 0; i < right; i++) {
if (a[i] > a[max]) {
max = i;
}
}
if(max != right) {
swap(a, max, right);
}
}
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
3) 堆排序
要点:
- 建立大顶堆
- 每次将堆顶元素(最大值)交换到末尾,调整堆顶元素,让它重新符合大顶堆特性
建堆
交换,下潜调整
代码
public class HeapSort {
public static void sort(int[] a) {
heapify(a, a.length);
for (int right = a.length - 1; right > 0; right--) {
swap(a, 0, right);
down(a, 0, right);
}
}
// 建堆 O(n)
private static void heapify(int[] array, int size) {
for (int i = size / 2 - 1; i >= 0; i--) {
down(array, i, size);
}
}
// 下潜
// leetcode 上数组排序题目用堆排序求解,非递归实现比递归实现大约快 6ms
private static void down(int[] array, int parent, int size) {
while (true) {
int left = parent * 2 + 1;
int right = left + 1;
int max = parent;
if (left < size && array[left] > array[max]) {
max = left;
}
if (right < size && array[right] > array[max]) {
max = right;
}
if (max == parent) { // 没找到更大的孩子
break;
}
swap(array, max, parent);
parent = max;
}
}
// 交换
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {2, 3, 1, 7, 6, 4, 5};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
4) 插入排序
要点
- 将数组分为两部分 [0 .. low-1] [low .. a.length-1]
- 左边 [0 .. low-1] 是已排序部分
- 右边 [low .. a.length-1] 是未排序部分
- 每次从未排序区域取出 low 位置的元素, 插入到已排序区域
例


代码
public class InsertionSort {
public static void sort(int[] a) {
for (int low = 1; low < a.length; low++) {
// 将 low 位置的元素插入至 [0..low-1] 的已排序区域
int t = a[low];
int i = low - 1; // 已排序区域指针
while (i >= 0 && t < a[i]) { // 没有找到插入位置
a[i + 1] = a[i]; // 空出插入位置
i--;
}
// 找到插入位置
if (i != low - 1) {
a[i + 1] = t;
}
}
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 2, 5, 8, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
5) 希尔排序
要点
- 简单的说,就是分组实现插入,每组元素间隙称为 gap
- 每轮排序后 gap 逐渐变小,直至 gap 为 1 完成排序
- 对插入排序的优化,让元素更快速地交换到最终位置
下图演示了 gap = 4,gap = 2,gap = 1 的三轮排序前后比较

代码
public class ShellSort {
public static void sort(int[] a) {
for (int gap = a.length>>1; gap >0 ; gap=gap>>1) {
for (int low = gap; low < a.length; low ++) {
// 将 low 位置的元素插入至 [0..low-1] 的已排序区域
int t = a[low];
int i = low - gap; // 已排序区域指针
while (i >= 0 && t < a[i]) { // 没有找到插入位置
a[i + gap] = a[i]; // 空出插入位置
i -= gap;
}
// 找到插入位置
if (i != low - gap) {
a[i + gap] = t;
}
}
}
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 2, 5, 8, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
6) 归并排序
递归实现
要点
- 分 - 每次从中间切一刀,处理的数据少一半
- 治 - 当数据仅剩一个时可以认为有序
- 合 - 两个有序的结果,可以进行合并排序(参见数组练习 E01. 合并有序数组)

代码
public class MergeSortTopDown {
/*
a1 原始数组
i~iEnd 第一个有序范围
j~jEnd 第二个有序范围
a2 临时数组
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
int k = i;
while (i <= iEnd && j <= jEnd) {
if (a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
if (i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd - j + 1);
}
if (j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
public static void sort(int[] a1) {
int[] a2 = new int[a1.length];
split(a1, 0, a1.length - 1, a2);
}
private static void split(int[] a1, int left, int right, int[] a2) {
int[] array = Arrays.copyOfRange(a1, left, right + 1);
// System.out.println(Arrays.toString(array));
// 2. 治
if (left == right) {
return;
}
// 1. 分
int m = (left + right) >>> 1;
split(a1, left, m, a2); // left = 0 m = 0 9
split(a1, m + 1, right, a2); // m+1 = 1 right = 1 3
// 3. 合
merge(a1, left, m, m + 1, right, a2);
System.arraycopy(a2, left, a1, left, right - left + 1);
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 2, 8, 5, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
时间复杂度
-
两个长度为 m 和 n 的链表合并,时间复杂度是 m + n
-
归并,时间复杂度:
f(n) = 2f(n/2) + n, f(1)=c,等价解f(n) = nlog_2{n} + cn8
/ \
4 4
/ \ / \
2 2 2 2
|| || || ||
11 11 11 11
f(8) = 2f(4) + 8
f(4) = 2f(2) + 4
f(2) = 2f(1) + 2
f(1) = 1
f(8) = 8 + 24
f(4) = 4 + 8
f(2) = 2 + 2
f(1) = 1- 当 n = 16 时,结果 80
- 当 n = 64 时,结果 448
-
若逐一合并,时间复杂度:
f(n)=\sum\limits_{n=0}^{n-1}n+1,等价解f(n)=\frac{1}{2}(n^2+n)1|0 => 1
1|1 => 2
1|2 => 3
1|3 => 4
1|4 => 5
1|5 => 6
1|6 => 7
1|7 => 8
36- 当 n = 16 时,结果 136
- 当 n = 64 时,结果 2080
非递归实现
public class MergeSortBottomUp {
/*
a1 原始数组
i~iEnd 第一个有序范围
j~jEnd 第二个有序范围
a2 临时数组
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
int k = i;
while (i <= iEnd && j <= jEnd) {
if (a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
if (i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd - j + 1);
}
if (j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
public static void sort(int[] a1) {
int n = a1.length;
int[] a2 = new int[n];
for (int width = 1; width < n; width *= 2) {
for (int i = 0; i < n; i += 2 * width) {
int m = Integer.min(i + width - 1, n - 1);
int j = Integer.min(i + 2 * width - 1, n - 1);
System.out.println(i + " " + m + " " + j);
merge(a1, i, m, m + 1, j, a2);
}
System.arraycopy(a2, 0, a1, 0, n);
}
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 2, 8, 5, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
7) 归并+插入
- 小数据量且有序度高时,插入排序效果高
- 大数据量用归并效果好
- 可以结合二者
public class MergeInsertionSort {
public static void insertion(int[] a, int left, int right) {
for (int low = left + 1; low <= right; low++) {
int t = a[low];
int i = low - 1;
while (i >= left && t < a[i]) {
a[i + 1] = a[i];
i--;
}
if (i != low - 1) {
a[i + 1] = t;
}
}
}
/*
a1 原始数组
i~iEnd 第一个有序范围
j~jEnd 第二个有序范围
a2 临时数组
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
int k = i;
while (i <= iEnd && j <= jEnd) {
if (a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
if (i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd - j + 1);
}
if (j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
public static void sort(int[] a1) {
int[] a2 = new int[a1.length];
split(a1, 0, a1.length - 1, a2);
}
private static void split(int[] a1, int left, int right, int[] a2) {
// int[] array = Arrays.copyOfRange(a1, left, right + 1);
// System.out.println(Arrays.toString(array));
// 2. 治
if (right == left) {
return;
}
if (right - left <= 32) {
insertion(a1, left, right);
System.out.println("insert..." + left + " " + right +" "+Arrays.toString(a1));
return;
}
// 1. 分
int m = (left + right) >>> 1;
split(a1, left, m, a2); // left = 0 m = 0 9
split(a1, m + 1, right, a2); // m+1 = 1 right = 1 3
System.out.println(left + " " + right + " "+Arrays.toString(a1));
// 3. 合
merge(a1, left, m, m + 1, right, a2);
System.arraycopy(a2, left, a1, left, right - left + 1);
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 2, 8, 5, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
8) 快速排序
单边循环(lomuto分区)要点
- 选择最右侧元素作为基准点
- j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
- 交换时机:j 找到小的,且与 i 不相等
- i 找到 >= 基准点元素后,不应自增
- 最后基准点与 i 交换,i 即为基准点最终索引
例:
i 和 j 都从左边出发向右查找,i 找到比基准点4大的5,j找到比基准点小的2,停下来交换

i 找到了比基准点大的5,j 找到比基准点小的3,停下来交换

j 到达right 处结束,right 与 i 交换,一轮分区结束

代码
public class QuickSortLomuto {
public static void sort(int[] a) {
quick(a, 0, a.length - 1);
}
private static void quick(int[] a, int left, int right) {
if (left >= right) {
return;
}
int p = partition(a, left, right); // p代表基准点元素索引
quick(a, left, p - 1);
quick(a, p + 1, right);
}
private static int partition(int[] a, int left, int right) {
int pv = a[right]; // 基准点元素值
int i = left;
int j = left;
while (j < right) {
if (a[j] < pv) { // j 找到比基准点小的了, 没找到大的
if (i != j) {
swap(a, i, j);
}
i++;
}
j++;
}
swap(a, i, right);
return i;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 9, 8, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
双边循环要点
- 选择最左侧元素作为基准点
- j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
- i 从左向右
- j 从右向左
- 最后基准点与 i 交换,i 即为基准点最终索引
例:
i 找到比基准点大的5停下来,j 找到比基准点小的1停下来(包含等于),二者交换

i 找到8,j 找到3,二者交换,i 找到7,j 找到2,二者交换

i == j,退出循环,基准点与 i 交换

代码
public class QuickSortHoare {
public static void sort(int[] a) {
quick(a, 0, a.length - 1);
}
private static void quick(int[] a, int left, int right) {
if (left >= right) {
return;
}
int p = partition(a, left, right);
quick(a, left, p - 1);
quick(a, p + 1, right);
}
private static int partition(int[] a, int left, int right) {
int i = left;
int j = right;
int pv = a[left];
while (i < j) {
while (i < j && a[j] > pv) {
j--;
}
while (i < j && pv >= a[i]) {
i++;
}
swap(a, i, j);
}
swap(a, left, j);
return j;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 2, 8, 5, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
随机基准点
使用随机数作为基准点,避免万一最大值或最小值作为基准点导致的分区不均衡
例

改进代码
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(a, idx, left);