Java中的Sort算法源码阅读笔记

133 阅读5分钟

注:本文的JDK版本为13

Java中为内置类型提供了排序算法,具体调用很简单,假如有一个int数组a,那么直接调用

Arrays.sort(a);

就能对数组完成排序,其余的内置类型也是如此。

Java有个命名习惯或者说是规范,后面加s的都是工具类,比如Arrays、Collections、Executors等等

那么,这个排序算法背后究竟干了哪些事?在时间上又是如何优化的呢?带着这个疑问,我进一步追了源码阅读。

/**
 * Sorts the specified array into ascending numerical order.
 *
 * <p>Implementation note: The sorting algorithm is a Dual-Pivot Quicksort
 * by Vladimir Yaroslavskiy, Jon Bentley, and Joshua Bloch. This algorithm
 * offers O(n log(n)) performance on many data sets that cause other
 * quicksorts to degrade to quadratic performance, and is typically
 * faster than traditional (one-pivot) Quicksort implementations.
 *
 * @param a the array to be sorted
 */
public static void sort(int[] a) {
    DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}

方法注释清清楚楚写了,在那些可能使其他快速排序降低性能的大部分数据集上,双轴快排仍然能够保证o(nlogn)的时间复杂度。在这里注明,该方法会默认进行升序排序。

java.util包下,DualPivotQuicksort类就实现了双轴快排算法,该类的Structure如下所示:

image.png

可以看到,最关键的方法就是sort()方法,我们以int数组为例,来进一步看该方法的源码。一百多行的代码比较长,为了方便阅读,我将其中的注释翻译成了中文。

/**
 * 情况允许的时候,使用给定的工作区数组切片对数组的指定范围进行排序以进行合并
 *
 * @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;

    // 检查数组是否几乎是有序的
    for (int k = left; k < right; run[count] = k) {
        // 首先排除数组开头的相同元素
        while (k < right && a[k] == a[k + 1])
            k++;
        if (k == right) break;  // 数组元素全部相同,那么检查完毕跳出for循环
        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;
            }
        }

        // 合并转换后的降序数组到升序数组上
        if (run[count] > left && a[run[count]] >= a[run[count] - 1]) {
            count--;
        }

        /*
         * 而如果这样的升序序列太多,那么说明数组十分无序,直接快排
         */
        if (++count == MAX_RUN_COUNT) {
            sort(a, left, right, true);
            return;
        }
    }

    // 下列条件应该成立:
    //    run[0] = 0
    //    run[<last>] = right + 1;(结束条件)

    if (count == 0) {
        // 数组已经有序
        return;
    } else if (count == 1 && run[count] > right) {
        // 单个升序或降序的数组切片
        // 前面提到了,run[<last>] = right + 1是结束条件,因此也可以直接返回
        return;
    }
    right++;
    if (run[count] < right) {
        // 这是一种特殊情况,最后一个升序序列刚好延申到数组结束,或最后只剩一个元素
        run[++count] = right;
    }

    // 定义合并的交替基准
    byte odd = 0;
    for (int n = 1; (n <<= 1) < count; odd ^= 1);

    // 使用或创建临时数组 b 进行合并
    int[] b;
    int ao, bo;              // 数组左偏移
    int blen = right - left; // b数组的长度
    if (work == null || workLen < blen || workBase + blen > work.length) {
        work = new int[blen];
        workBase = 0;
    }
    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], 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;
    }
}

首先就做了一个判断,如果是小数组,就直接快排:

// 如果是小数组,直接快排
if (right - left < QUICKSORT_THRESHOLD) {
    sort(a, left, right, true);
    return;
}

否则的话,源码中创建了一个run数组。

int[] run = new int[MAX_RUN_COUNT + 1];
int count = 0; run[0] = left;

这个数组的含义不难理解,只需要看MAX_RUN_COUNT变量的注释含义即可,可以看出是和合并数组有关的,具体作用还需要结合后面的代码来理解。

/**
 * The maximum number of runs in merge sort.
 */
private static final int MAX_RUN_COUNT = 67;

接下来,源码中做了一个检查,来看看这个数组是否几乎是有序的。

// 检查数组是否几乎是有序的
for (int k = left; k < right; run[count] = k) {
    // 首先排除数组开头的相同元素
    while (k < right && a[k] == a[k + 1])
        k++;
    if (k == right) break;  // 数组元素全部相同,那么检查完毕跳出for循环
    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;
        }
    }

    // 合并转换后的降序到升序数组上(只是一个记录索引的过程,并不在这里真正合并)
    // 不要忘记for循环中的run[count] = k语句,应该是在这里更新run数组来记录索引
    if (run[count] > left && a[run[count]] >= a[run[count] - 1]) {
        count--;
    }

    /*
     * 而如果这样的升序序列太多,那么说明数组十分无序,直接快排
     */
    if (++count == MAX_RUN_COUNT) {
        sort(a, left, right, true);
        return;
    }
}

个人理解的过程是这样,找到一段一段升序或降序的数组,然后每次都记录索引到run数组中,当run数组满了,也就是说明数组太过于无序,这个时候应该直接采用快排。

关于为什么直接采用快排,这里还需要了解一下快排与归并排序的一点区别,在数组无序时,快排要表现得比归并出色

如果能够跳出循环,就代表数组较为有序,我们继续看源码是如何处理的。首先是做了一系列的判断处理:

// 下列条件应该成立:
//    run[0] = 0
//    run[<last>] = right + 1;(结束条件)

if (count == 0) {
    // 数组已经有序
    return;
} else if (count == 1 && run[count] > right) {
    // 单个升序或降序的数组切片
    // 前面提到了,run[<last>] = right + 1是结束条件,因此也可以直接返回
    return;
}
right++;
if (run[count] < right) {
    // 这是一种特殊情况,最后一个升序序列刚好延申到数组结束,或最后只剩一个元素
    run[++count] = right;
}

这里要记住,run数组记录的是递增的序列片段索引,比如下列初始化的数组

int[] test = new int[300];
for (int i = 0; i < 300; i++) {
    test[i] = i % 30;
}

这个数组每隔30个元素就递增,那么run数组就应该是[0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],处理完之后,首先申请额外数组空间,以便之后的排序。

 // 定义合并的交替基准
byte odd = 0;
for (int n = 1; (n <<= 1) < count; odd ^= 1);

// 使用或创建临时数组 b 进行合并
int[] b;
int ao, bo;              // 数组左偏移
int blen = right - left; // b数组的长度
if (work == null || workLen < blen || workBase + blen > work.length) {
    work = new int[blen];
    workBase = 0;
}
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;
}

再申请好工作数组之后,就是需要根据归并的思想,从run数组中取出一段段递增序列然后不断合并到工作数组中,最后再将合并后的数组赋值给原数组,就完成了排序。

for (int last; count > 1; count = last) {
    for (int k = (last = 0) + 2; k <= count; k += 2) {
        int hi = run[k], 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;
}

至此,这个方法就结束了,不过上面只是分析了归并排序的情况,之前提到过,如果是小数组或者数组太过无序,那么就直接快排,这里的快排与普通快排不同,采用了双轴快排的思想。

双轴快排的思想是再快排的基础上,取最大索引和最小索引上的值,根据这两个值将数组分为三个部分,然后再对三个部分进行双轴快排,具体细节与源码就不在本文中展开了。