首页 > Java > 正文

Timsort算法浅析

标签:java, 算法

 上一篇谈到的双轴快排,是Arrays对八种基本类型进行排序的算法,针对其它的对象类型,JDK1.6及以前的版本使用的是归并排序,从JDK1.7开始,默认情况下会采用Timsort排序算法,而Collections.sort实际上也是调用Arrays.sort方法。现实中的大多数据通常是有部分已经排好序的,该算法利用这一特点提升了排序效率,下面将跟随JDK1.8源码,对Timsort的实现进行分析。

 排序的核心代码从TimSort.sort方法开始,首先判断需要排序的元素个数,如果小于一个阈值(在Tim的C语言实现中默认为64,JDK中为32),先找出从起点位置开始的最大升序或严格降序列长度,并对降序列进行翻转,然后进行二分插入排序。

    // If array is small, do a "mini-TimSort" with no merges
    if (nRemaining < MIN_MERGE) {
        int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
        // Binary insertion sort
        binarySort(a, lo, hi, lo + initRunLen, c);
        return;
    }

 如果长度大于这个阈值,开始从左往右遍历数组,找出一个升序列或严格降序列,同样需要对降序列进行翻转,得到的序列这里称为一个run。如果这个run的长度太小,用二分插入排序进行增补,再入栈并尝试合并,然后继续往右寻找下一个run。

    /**
     * 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);

 每次有新的run入栈,都会对栈里相邻的两个run按照一定规则尝试进行合并,使得对于任意相邻的三个run,它们的长度X、Y、Z满足两个条件:

  1. X > Y + Z
  2. Y > Z

 具体做法是,从栈顶的三个run开始判断,如果没有同时满足两个条件,就通过合并把两个run变为一个更大的run,然后继续循环对栈顶三个run进行判断。

    /**
     * Examines the stack of runs waiting to be merged and merges adjacent runs
     * until the stack invariants are reestablished:
     *
     *     1. runLen[i - 3] > runLen[i - 2] + runLen[i - 1]
     *     2. runLen[i - 2] > runLen[i - 1]
     *
     * This method is called each time a new run is pushed onto the stack,
     * so the invariants are guaranteed to hold for i < stackSize upon
     * entry to the method.
     */
    private void mergeCollapse() {
        while (stackSize > 1) {
            int n = stackSize - 2;
            if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) {
                if (runLen[n - 1] < runLen[n + 1])
                    n--;
                mergeAt(n);
            } else if (runLen[n] <= runLen[n + 1]) {
                mergeAt(n);
            } else {
                break; // Invariant is established
            }
        }
    }

 为了提高效率,在合并两个相邻run的时候,采用了一个叫Galloping的模型进行优化。例如现在要合并较小的A和较大的B两个run,因为A和B已经分别是排好序的,用二分查找找到B的第一个元素在A中何处插入,找到以后,A在该素左边的部分就不需要比较了。同样,找到A的最后一个元素在B的何处插入,B在该元素之后的元素也不需要比较了。

    /*
     * Find where the first element of run2 goes in run1. Prior elements
     * in run1 can be ignored (because they're already in place).
     */
    int k = gallopRight(a[base2], a, base1, len1, 0, c);
    assert k >= 0;
    base1 += k;
    len1 -= k;
    if (len1 == 0)
        return;
    /*
     * Find where the last element of run1 goes in run2. Subsequent elements
     * in run2 can be ignored (because they're already in place).
     */
    len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
    assert len2 >= 0;
    if (len2 == 0)
        return;

 接下来合并中间的部分,用一个临时存储空间,大小是待合并的两部分中较小的部分的大小。先将较小的部分复制到这个临时存储空间,然后用原先存储这2个部分的空间来存储合并后的元素。剩下就是简单归并排序:依次从左到右或从右到左,不断比较两个序列的第一个元素并插入到相应位置。

    // Merge remaining runs, using tmp array with min(len1, len2) elements
    if (len1 <= len2)
        mergeLo(base1, len1, base2, len2);
    else
        mergeHi(base1, len1, base2, len2);

 总结:Timsort结合了归并排序和插入排序,很好地利用了无序数列中有序的部分,按照升序和降序子序列进行分区,减少了升序部分的回溯和降序部分的性能倒退。Galloping模型的引入也降低了不必要的比较次数,大大优化了两个相邻run的合并效率。


原创文章,转载请注明出处!
本文链接:http://wuukee.github.io/posts/timsort.html
上篇: 双轴快排原理解析
下篇: 数据库的事务隔离级别