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)

posted @ 2025-12-09 00:55  xxs不是小学生  阅读(1)  评论(0)    收藏  举报