Java 中 Arrays.sort() 的底层实现
文章目录
- Java 中 Arrays.sort()的底层实现
- 1.基本类型排序:双轴快速排序(Dual-Pivot Quicksort)
- 2.对象类型排序:TimSort(归并排序 + 插入排序混合)
- 3.并行排序:`Arrays.parallelSort()`
Java 中 Arrays.sort()的底层实现

Arrays.sort() 是 Java 中最常用的排序方法之一,但它的底层实现其实 并不只有一种,而是根据 数据类型 和 数据规模 来动态选择不同的排序算法。
| 数据类型 | 使用的排序算法 | 是否稳定 | 底层类 |
|---|---|---|---|
| 基本类型(int、double、char 等) | 双轴快速排序(Dual-Pivot Quicksort) | 否 | DualPivotQuicksort |
对象类型(如 String、Integer、自定义类等) | TimSort(归并+插入排序混合) | 是 | TimSort |
| 并行排序 | 并行归并排序(ForkJoin 框架) | 是 | Arrays.parallelSort() |
1.基本类型排序:双轴快速排序(Dual-Pivot Quicksort)
DualPivotQuicksort 是改进版快速排序。与传统单轴快排不同,它使用 两个枢轴(pivot1 和 pivot2) 来将数组划分为 三个区域,然后递归地分别排序这三个区域。
[ < pivot1 ] [ pivot1 ≤ x ≤ pivot2 ] [ > pivot2 ]
平均时间复杂度:O(n log n)
源码原理:
- 对于小数组(长度小于
QUICKSORT_THRESHOLD(286)),直接使用快速排序。
- 对于小数组(长度小于
INSERTION_SORT_THRESHOLD(47)),使用插入排序。
- 当
leftmost为true时,使用 传统插入排序。- 当
leftmost为false时,使用优化的 成对插入排序。(利用相邻部分作为哨兵)- 否则,使用快速排序。 从数组中均匀选取 5 个样本元素(包括中间元素)对这些样本元素进行排序,根据样本元素的分布情况决定使用双轴还是单轴策略。
- 双轴快速排序逻辑:使用两个枢轴(
pivot1和pivot2,其中pivot1 <= pivot2)。将数组分成三部分:小于pivot1、介于pivot1和pivot2之间、大于pivot2。递归排序这三个部分。- 单轴快速排序逻辑:当数组中存在大量重复元素时使用。将数组分成三部分:小于枢轴、等于枢轴、大于枢轴,只需要递归排序小于和大于部分,提高效率。
- 对于较大数组,首先尝试识别数组中的有序段(run),然后进行合并排序。
- 扫描数组,识别其中的有序段(升序、降序或相等元素序列),对于降序段,会将其转换为升序,对于相等元素过多的情况,直接回退到快速排序,如果识别出的有序段数量超过
MAX_RUN_COUNT,也回退到快速排序。(预处理阶段)- 检查特殊情况(如数组已排序或只有一个元素),确定归并排序的基础(使用临时数组或原数组),根据需要创建或使用工作数组。(归并排序准备)
- 成对合并已识别的有序段,在原数组和临时数组之间交替进行归并。(归并阶段)
源码注释如下:
/*** 对数组的指定范围进行排序,尽可能使用给定的工作数组片段进行合并操作** @param a 要排序的数组* @param left 要排序的第一个元素的索引(包含)* @param right 要排序的最后一个元素的索引(包含)* @param work 工作数组(片段)* @param workBase 工作数组中可用空间的起始位置* @param workLen 工作数组的可用大小*/
static void sort(int[] a, int left, int right, int[] work, int workBase, int workLen) {// 对于小数组,使用快速排序// 小数组使用快速排序效率更高,避免归并排序的额外开销if (right - left < QUICKSORT_THRESHOLD) {// 调用内部的快速排序方法sort(a, left, right, true);return;}/** run[i] 存储第i个有序段的起始索引* 有序段可以是升序或降序序列*/// 创建存储有序段起始位置的数组int[] run = new int[MAX_RUN_COUNT + 1];int count = 0; // 有序段计数run[0] = left; // 第一个有序段从left开始// 检查数组是否接近有序,识别所有有序段for (int k = left; k < right; run[count] = k) {if (a[k] < a[k + 1]) { // 升序序列// 扩展升序序列,直到遇到降序或结束while (++k <= right && a[k - 1] <= a[k]);} else if (a[k] > a[k + 1]) { // 降序序列// 扩展降序序列,直到遇到升序或结束while (++k <= right && a[k - 1] >= a[k]);// 将降序序列反转成升序for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {int t = a[lo]; a[lo] = a[hi]; a[hi] = t;}} else { // 相等元素序列// 处理相等元素的情况// 限制相等元素序列的最大长度,超过阈值则切换到快速排序for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) {if (--m == 0) {sort(a, left, right, true);return;}}}/** 如果识别的有序段数量超过最大值,说明数组结构不够好* 此时使用快速排序替代归并排序更高效*/if (++count == MAX_RUN_COUNT) {sort(a, left, right, true);return;}}// 检查特殊情况// 注意:此处变量"right"会增加1if (run[count] == right++) { // 最后一个有序段只包含一个元素run[++count] = right;} else if (count == 1) { // 整个数组已经排序return;}// 确定归并的交替基础// 通过计算确定最终归并结果应该放在哪个数组byte odd = 0;for (int n = 1; (n <<= 1) < count; odd ^= 1);// 使用或创建用于归并的临时数组bint[] b; // 临时数组,与a交替使用int ao, bo; // 相对于'left'的数组偏移量int blen = right - left; // b所需的空间大小// 检查工作数组是否可用,否则创建新的工作数组if (work == null || workLen < blen || workBase + blen > work.length) {work = new int[blen];workBase = 0;}// 根据前面计算的odd值,决定归并的起始数组if (odd == 0) {// 将原数组内容复制到工作数组,原数组将用作目标数组System.arraycopy(a, left, work, workBase, blen);b = a;bo = 0;a = work;ao = workBase - left;} else {// 使用工作数组作为目标数组b = work;ao = 0;bo = workBase - left;}// 归并过程:循环合并有序段,直到只剩下一个有序段for (int last; count > 1; count = last) {// 两两合并有序段for (int k = (last = 0) + 2; k <= count; k += 2) {int hi = run[k]; // 当前要合并的两个段的结束位置int mi = run[k - 1]; // 两个段的中间位置// 执行合并操作,将两个有序段合并为一个for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) {// 比较两个段的元素,选择较小的放入目标数组if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) {b[i + bo] = a[p++ + ao];} else {b[i + bo] = a[q++ + ao];}}run[++last] = hi; // 记录合并后的段结束位置}// 处理奇数个有序段的情况,将最后一个未合并的段直接复制到目标数组if ((count & 1) != 0) {for (int i = right, lo = run[count - 1]; --i >= lo;b[i + bo] = a[i + ao]);run[++last] = right;}// 交换数组引用,为下一轮合并做准备int[] t = a; a = b; b = t;int o = ao; ao = bo; bo = o;}
}
/*** 使用双枢轴快速排序算法对数组的指定范围进行排序** @param a 要排序的数组* @param left 要排序的第一个元素的索引(包含)* @param right 要排序的最后一个元素的索引(包含)* @param leftmost 指示此部分是否在整个排序范围的最左侧*/
private static void sort(int[] a, int left, int right, boolean leftmost) {int length = right - left + 1;// 对于极小数组,使用插入排序// 小数组使用插入排序效率更高,因为其常数因子小if (length < INSERTION_SORT_THRESHOLD) {if (leftmost) {/** 对最左侧部分使用传统(无哨兵)的插入排序,* 此实现针对服务器VM进行了优化*/for (int i = left, j = i; i < right; j = ++i) {int ai = a[i + 1];// 将当前元素插入到前面已排序部分的正确位置while (ai < a[j]) {a[j + 1] = a[j]; // 元素右移if (j-- == left) { // 到达左边界break;}}a[j + 1] = ai; // 插入元素}} else {/** 跳过最长的升序序列*/do {if (left >= right) {return;}} while (a[++left] >= a[left - 1]);/** 利用相邻部分的元素作为哨兵,避免每次迭代的左边界检查* 此外,使用优化的成对插入排序算法,在快速排序的上下文中* 比传统插入排序实现更快*/for (int k = left; ++left <= right; k = ++left) {int a1 = a[k], a2 = a[left];// 确保a1 >= a2if (a1 < a2) {a2 = a1; a1 = a[left];}// 将较大的元素a1插入到正确位置while (a1 < a[--k]) {a[k + 2] = a[k];}a[++k + 1] = a1;// 将较小的元素a2插入到正确位置while (a2 < a[--k]) {a[k + 1] = a[k];}a[k + 1] = a2;}// 处理最后一个元素int last = a[right];while (last < a[--right]) {a[right + 1] = a[right];}a[right + 1] = last;}return;}// 计算length/7的近似值,使用位运算提高效率int seventh = (length >> 3) + (length >> 6) + 1;/** 对范围中心及其周围均匀分布的五个元素进行排序* 这些元素将用于下面的枢轴选择* 元素间距的选择是通过实验确定的,适用于各种输入*/int e3 = (left + right) >>> 1; // 中心点int e2 = e3 - seventh;int e1 = e2 - seventh;int e4 = e3 + seventh;int e5 = e4 + seventh;// 使用插入排序对这五个样本元素进行排序// 通过一系列比较和交换确保这五个元素有序if (a[e2] < a[e1]) { int t = a[e2]; a[e2] = a[e1]; a[e1] = t; }if (a[e3] < a[e2]) { int t = a[e3]; a[e3] = a[e2]; a[e2] = t;if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }}if (a[e4] < a[e3]) { int t = a[e4]; a[e4] = a[e3]; a[e3] = t;if (t < a[e2]) { a[e3] = a[e2]; a[e2] = t;if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }}}if (a[e5] < a[e4]) { int t = a[e5]; a[e5] = a[e4]; a[e4] = t;if (t < a[e3]) { a[e4] = a[e3]; a[e3] = t;if (t < a[e2]) { a[e3] = a[e2]; a[e2] = t;if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }}}}// 设置指针int less = left; // 中间部分第一个元素的索引int great = right; // 右侧部分第一个元素之前的索引// 如果五个样本元素都不相等,则使用双枢轴排序if (a[e1] != a[e2] && a[e2] != a[e3] && a[e3] != a[e4] && a[e4] != a[e5]) {/** 使用五个已排序元素中的第二个和第四个作为枢轴* 这些值是数组第一和第二个三分位数的近似值* 注意pivot1 <= pivot2*/int pivot1 = a[e2];int pivot2 = a[e4];/** 将待排序的第一个和最后一个元素移到枢轴原来的位置* 当分区完成后,枢轴将交换回它们的最终位置,* 并排除在后续排序之外*/a[e2] = a[left];a[e4] = a[right];/** 跳过已经小于pivot1或大于pivot2的元素*/while (a[++less] < pivot1);while (a[--great] > pivot2);/** 分区过程:** 左侧部分 中间部分 右侧部分* +--------------------------------------------------------------+* | < pivot1 | pivot1 <= && <= pivot2 | ? | > pivot2 |* +--------------------------------------------------------------+* ^ ^ ^* | | |* less k great** 不变式:** 所有在(left, less)中的元素 < pivot1* pivot1 <= 所有在[less, k)中的元素 <= pivot2* 所有在(great, right)中的元素 > pivot2** 指针k是?-部分的第一个索引*/outer:for (int k = less - 1; ++k <= great; ) {int ak = a[k];if (ak < pivot1) { // 将a[k]移到左侧部分a[k] = a[less];/** 这里和下面使用"a[i] = b; i++"而不是* "a[i++] = b"是出于性能考虑*/a[less] = ak;++less;} else if (ak > pivot2) { // 将a[k]移到右侧部分while (a[great] > pivot2) {if (great-- == k) {break outer;}}if (a[great] < pivot1) { // a[great] <= pivot2a[k] = a[less];a[less] = a[great];++less;} else { // pivot1 <= a[great] <= pivot2a[k] = a[great];}/** 这里和下面使用"a[i] = b; i--"而不是* "a[i--] = b"是出于性能考虑*/a[great] = ak;--great;}}// 将枢轴交换到它们的最终位置a[left] = a[less - 1]; a[less - 1] = pivot1;a[right] = a[great + 1]; a[great + 1] = pivot2;// 递归排序左侧和右侧部分,排除已知的枢轴sort(a, left, less - 2, leftmost);sort(a, great + 2, right, false);/** 如果中间部分太大(占数组的比例超过4/7),* 将内部的枢轴值交换到两端*/if (less < e1 && e5 < great) {/** 跳过等于枢轴值的元素*/while (a[less] == pivot1) {++less;}while (a[great] == pivot2) {--great;}/** 分区:** 左侧部分 中间部分 右侧部分* +----------------------------------------------------------+* | == pivot1 | pivot1 < && < pivot2 | ? | == pivot2 |* +----------------------------------------------------------+* ^ ^ ^* | | |* less k great** 不变式:** 所有在(*, less)中的元素 == pivot1* pivot1 < 所有在[less, k)中的元素 < pivot2* 所有在(great, *)中的元素 == pivot2** 指针k是?-部分的第一个索引*/outer:for (int k = less - 1; ++k <= great; ) {int ak = a[k];if (ak == pivot1) { // 将a[k]移到左侧部分a[k] = a[less];a[less] = ak;++less;} else if (ak == pivot2) { // 将a[k]移到右侧部分while (a[great] == pivot2) {if (great-- == k) {break outer;}}if (a[great] == pivot1) { // a[great] < pivot2a[k] = a[less];/** 尽管a[great]等于pivot1,但赋值a[less] = pivot1* 可能不正确,如果a[great]和pivot1是符号不同的浮点零* 因此在float和double排序方法中,我们必须使用更准确的* 赋值a[less] = a[great]*/a[less] = pivot1;++less;} else { // pivot1 < a[great] < pivot2a[k] = a[great];}a[great] = ak;--great;}}}// 递归排序中间部分sort(a, less, great, false);} else { // 使用单枢轴分区/** 使用五个已排序元素中的第三个作为枢轴* 这个值是中位数的近似值*/int pivot = a[e3];/** 分区退化为传统的三向切分(或"荷兰国旗")模式:** 左侧部分 中间部分 右侧部分* +-------------------------------------------------+* | < pivot | == pivot | ? | > pivot |* +-------------------------------------------------+* ^ ^ ^* | | |* less k great** 不变式:** 所有在(left, less)中的元素 < pivot* 所有在[less, k)中的元素 == pivot* 所有在(great, right)中的元素 > pivot** 指针k是?-部分的第一个索引*/for (int k = less; k <= great; ++k) {if (a[k] == pivot) {continue;}int ak = a[k];if (ak < pivot) { // 将a[k]移到左侧部分a[k] = a[less];a[less] = ak;++less;} else { // a[k] > pivot - 将a[k]移到右侧部分while (a[great] > pivot) {--great;}if (a[great] < pivot) { // a[great] <= pivota[k] = a[less];a[less] = a[great];++less;} else { // a[great] == pivot/** 尽管a[great]等于pivot,但赋值a[k] = pivot* 可能不正确,如果a[great]和pivot是符号不同的浮点零* 因此在float和double排序方法中,我们必须使用更准确的* 赋值a[k] = a[great]*/a[k] = pivot;}a[great] = ak;--great;}}/** 递归排序左侧和右侧部分* 中间部分的所有元素都相等,因此已经排序*/sort(a, left, less - 1, leftmost);sort(a, great + 1, right, false);}
}
2.对象类型排序:TimSort(归并排序 + 插入排序混合)
TimSort 是一种 混合排序算法,它结合了 归并排序(稳定、高效) 和 插入排序(对小规模数据高效) 的优点。
Java 在排序 Object[] 时调用:Arrays.sort(Object[] a) 底层实际上是:TimSort.sort(a, 0, a.length, null, 0, 0);
TimSort 先将数组划分成若干个小块(称为 run),每个 run 都是一个 已排序序列(自然有序或通过插入排序得到)。然后利用归并思想合并这些 run。
原数组 → 分割为 runs → 对每个 run 插入排序 → 合并相邻 runs(归并)
平均时间复杂度:O(n log n)
3.并行排序:Arrays.parallelSort()
底层使用 ForkJoin 框架 实现并行归并排序:
将数组递归地分割为多个子数组 -> 每个子任务并行排序 -> 使用归并算法合并结果
平均时间复杂度:O(n log n)
总结:Arrays.sort() 对基本类型用双轴快排,对对象类型用 TimSort,对大数组可以用 parallelSort 并行归并。
🐮🐴
