Collections.sort源码分析

Collections.sort

Collections类是Collection类及其子类的工具类,其中的sort方法专门用于对集合中的元素进行排序,该方法主要有两种重载的方法,可以指定显式的比较器,也可以不指定,但是待比较集合中的元素必须是继承了Comparable接口:

public static <T extends Comparable<? super T>> void sort(List<T> list) {
    list.sort(null);
}
public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

这两个方法底层调用的都是List接口中的sort方法(当然,也有List接口的实现类重写了这个方法,不过也都是调用的Arrays类的sort方法):

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

而在该方法中首先会将待排序集合转换为数组,然后调用数组工具类的sort方法,按照指定的比较器对象的比较规则对数组中的元素进行排序:

public static <T> void sort(T[] a, Comparator<? super T> c) {
    if (c == null) {
        sort(a);
    } else {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a, c);
        else
            TimSort.sort(a, 0, a.length, c, null, 0, 0);
    }
}

该方法要求数组中所有元素必须基于指定的Comparator是可相互比较的,也就是说不能抛出ClassCastException异常。

此方法的实现说明:此实现是一种稳定的、自适应的、迭代的归并排序,当输入数组部分排序时,它需要的比较次数远少于 nlg(n) 次。同时当输入数组随机排列时提供传统归并排序的性能(nlog(n))。如果输入数组几乎已排序,则实现排序需要大约n次比较。需要的临时存储空间从几乎有序的输入数组的较小常量值到随机排列的输入数组的n/2个对象引用不等。

该实现在其输入数组中平等地利用升序和降序,并且可以在同一输入数组的不同部分利用升序和降序。它非常适合合并两个或多个已排序的数组:只需连接数组并对结果数组进行排序。

在此方法中会根据情况选择不同的排序方法,其中LegacyMergeSort.userRequested是一个布尔值:

    /**
     * Old merge sort implementation can be selected (for
     * compatibility with broken comparators) using a system property.
     * Cannot be a static boolean in the enclosing class due to
     * circular dependencies. To be removed in a future release.
     */
    static final class LegacyMergeSort {
        private static final boolean userRequested =
            java.security.AccessController.doPrivileged(
                new sun.security.action.GetBooleanAction(
                    "java.util.Arrays.useLegacyMergeSort")).booleanValue();
    }

它其实是一个开关参数,将它设置为true,可以使用旧版本的归并排序算法对数组中的元素进行排序。不过它将在未来版本中被删除。

排序方法1

如果比较器对象为null,如果没有传入比较器,那么执行的sort方法为:

public static void sort(Object[] a) {
    if (LegacyMergeSort.userRequested)
        legacyMergeSort(a);
    else
        ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}

ComparableTimSort.sort方法的源码:

static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
    assert a != null && lo >= 0 && lo <= hi && hi <= a.length;

    int nRemaining  = hi - lo;
    if (nRemaining < 2)
        return;  // Arrays of size 0 and 1 are always sorted

    // If array is small, do a "mini-TimSort" with no merges
    if (nRemaining < MIN_MERGE) {
        //这个方法比较有意思,其实就是将我们最长的递减序列,找出来,然后倒过来
        int initRunLen = countRunAndMakeAscending(a, lo, hi);
        binarySort(a, lo, hi, lo + initRunLen);
        return;
    }

    /**
     * March over the array once, left to right, finding natural runs,
     * extending short natural runs to minRun elements, and merging runs
     * to maintain stack invariant.
     */
    ComparableTimSort ts = new ComparableTimSort(a, work, workBase, workLen);
    int minRun = minRunLength(nRemaining);
    do {
        // Identify next run
        int runLen = countRunAndMakeAscending(a, lo, hi);

        // If run is short, extend to min(minRun, nRemaining)
        if (runLen < minRun) {
            int force = nRemaining <= minRun ? nRemaining : minRun;
            binarySort(a, lo, lo + force, lo + runLen);
            runLen = force;
        }

        // Push run onto pending-run stack, and maybe merge
        ts.pushRun(lo, runLen);
        ts.mergeCollapse();

        // Advance to find next run
        lo += runLen;
        nRemaining -= runLen;
    } while (nRemaining != 0);

    // Merge all remaining runs to complete sort
    assert lo == hi;
    ts.mergeForceCollapse();
    assert ts.stackSize == 1;
}

排序方法2

如果比较器不为null,即我们指定了比较器对象,那么调用的方法sort方法是TimSort.sort方法:

static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
                     T[] work, int workBase, int workLen) {
    assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;

    int nRemaining  = hi - lo;
    if (nRemaining < 2)
        return;  // Arrays of size 0 and 1 are always sorted

    // If array is small, do a "mini-TimSort" with no merges
    if (nRemaining < MIN_MERGE) {
        //这个方法比较有意思,其实就是将我们最长的递减序列,找出来,然后倒过来
        int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
        binarySort(a, lo, hi, lo + initRunLen, c);
        return;
    }

    /**
     * March over the array once, left to right, finding natural runs,
     * extending short natural runs to minRun elements, and merging runs
     * to maintain stack invariant.
     */
    TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);
    int minRun = minRunLength(nRemaining);
    do {
        // Identify next run
        int runLen = countRunAndMakeAscending(a, lo, hi, c);

        // If run is short, extend to min(minRun, nRemaining)
        if (runLen < minRun) {
            int force = nRemaining <= minRun ? nRemaining : minRun;
            binarySort(a, lo, lo + force, lo + runLen, c);
            runLen = force;
        }

        // Push run onto pending-run stack, and maybe merge
        ts.pushRun(lo, runLen);
        ts.mergeCollapse();

        // Advance to find next run
        lo += runLen;
        nRemaining -= runLen;
    } while (nRemaining != 0);

    // Merge all remaining runs to complete sort
    assert lo == hi;
    ts.mergeForceCollapse();
    assert ts.stackSize == 1;
}

可以看出ComparableTimSort.sort方法与TimSort.sort方法几乎一样,主要的区别就在于一个没有显示指定比较器,一个显示地指定了比较器对象。没有显示指定比较器对象的时,则需要待排序集合元素继承了Comparable接口,然后自身实现了一些比较的规则。

在这两个方法中,如果待排序的数组较小,小于阈值32,则会采用一个“mini-TimSort”的算法,而无需合并操作。

此时会执行一个方法:

countRunAndMakeAscending(a, lo, hi);  //
countRunAndMakeAscending(a, lo, hi, c); //

此方法实现的功能是:返回从指定数组中的指定位置开始的run长度,如果run是降序则反转run(确保在方法返回时运行始终是升序)。

run是最长的升序序列:a[lo] <= a[lo + 1] <= a[lo + 2] <= ...

或最长的降序序列:a[lo] > a[lo + 1] > a[lo + 2] > ...

然后执行binarySort方法:

使用二进制插入排序对指定数组的指定部分进行排序。这是对少量元素进行排序的最佳方法。它需要 O(nlogn) 比较,但需要 O(n^2) 数据移动(最坏情况)。

如果待排序数组大于32时, 先算出一个合适的大小,再将输入按其升序和降序特点进行分区。排序的输入单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这些run序列,每次拿一个run出来按规则进行合并。每次合并会将两个run合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。

posted @ 2021-08-01 10:30  有心有梦  阅读(231)  评论(0编辑  收藏  举报