Arrays.sort 源码解析
简介
Arrays.sort底层并不是基于简单的快速排序算法,而是根据工程实践进行了优化,针对不同的数据类型,选择不同的排序算法:
- 基本数据类型:双轴快速排序算法
- 引用数据类型:TimSort算法
双轴快速排序 DualPivotQuicksort
对于基本数据类型的排序,JDK进行了优化,使用双轴快速排序,每次选择两个轴,相比于单轴快速排序,比较次数和树高都更少,效率更高。但是和快排一样,是一个不稳定的算法。
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
在JDK源码中,用于对基本类型排序的类为DualPivotQuicksort,通常采用混合排序策略,对不同规模的数组采用不同的排序算法,提高效率.具体参考DualPivotQuicksort。
Arrays.sort对基本数据类型采用混合排序算法,针对不同长度的数组采用不同的排序算法:
- 数组长度小于47时采用插入排序,插入排序适合小数组,同时常数因子更小
- 数组长度大于47且小于286时采用双轴快速排序,数据规模中等,快排的递归深度不会太大,效率更高
- 数组长度大于286采用TimSort,能够利用数组局部有序的特性,进一步提高效率
简单说一下这个代码
/**
当数组长度小于286时,直接使用双轴快排,但其实这里的sort内部进行了进一步划分,当小于47时直接采用插入排序
*/
if (right - left < QUICKSORT_THRESHOLD) {
sort(a, left, right, true);
return;
}
如果大于286的话会采用TimSort算法,简单来说就是寻找有序段,然后进行归并排序。但是如果有序段太多,会降级为双轴快排。JDK默认的最大有序段数量为67,单个有序段的最大长度为33。
int[] run = new int[MAX_RUN_COUNT + 1];
int count = 0;
run[0] = left;
for (int k = left; k < right; run[count] = k) {
if (a[k] < a[k + 1]) { // 升序 run
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], 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) { // 相等元素太多,放弃 run 检测
sort(a, left, right, true);
return;
}
}
}
// 如果 run 太多(> MAX_RUN_COUNT),说明无序,回退到快排
if (++count == MAX_RUN_COUNT) {
sort(a, left, right, true);
return;
}
}
总结来说的话就是:
| 场景 | 策略 | 原因 |
|---|---|---|
| 小数组 | 插入排序 | 低开销,缓存友好 |
| 高度无序大数组或中规模数组 | 回退到 Dual-Pivot Quicksort | 快排平均性能最优 |
| 接近有序大数组 | Run detection + 归并 | 归并在有序数据上接近 O(n),且稳定 |
ComparableTimSort
和基本数据类型不同,基本数据类型相等就代表语义完全相同,但是引用数据类型不同,equals函数返回相等,并不代表语义完全相等,所以引用数据类型的排序需要保证稳定性。针对引用类型,JDK采用TimSort算法,参考TimSort
public static void sort(Object[] a) {
if (Arrays.LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
引用类型需要实现Comparable接口
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
引用类型排序也是一个混合排序算法:
- 当数组长度小于32时,直接使用二分插入排序
- 数组长度大于32时,采用完整版TimSort算法
do {
// 1. 识别下一个自然 run
int runLen = countRunAndMakeAscending(a, lo, hi);
// 2. 如果 run 太短,用 binarySort 扩展到 minRun
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen);
runLen = force;
}
// 3. 将 run 压入栈,并尝试合并(维持栈不变式)
ts.pushRun(lo, runLen);
ts.mergeCollapse();
// 4. 移动指针
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
Timsort简单来说就是识别有序段之后归并排序的过程,有序段不能太短,如果太短则使用后边的元素通过二分插入排序插入,稳定且最坏时间复杂度为 O(n log n)

浙公网安备 33010602011771号