TimSort算法(JDK)

算法介绍

JDK1.8中,对于列表的排序,java.util.List中提供了sort方法,调用的Arrays.sort(T[],Comparator<? super T>),Arrays提供的对Object的一种排序方法(这里用的是泛型T,还有Object[]对应的排序方法),在该方法中可以看到使用的是TimSort类的静态方法对数组进行排序,TimSort类的内容就是TimSort算法的实现。

TimSort是一种混合排序算法,内部使用了插入排序(二分插入排序)和归并排序,是在数组中的数据都是部分有序(正序或倒序)的情况下,将两种排序算法进行结合优化带来排序效率的提高。

对于插入排序,其时间复杂度为O(n^2),但在数据基本有序的情况下有教好的排序性能,而二分插入排序是对插入排序的优化,减少比较次数,但没有减少移动次数,时间复杂度为O(n*logn)。

对于归并排序,其时间复杂度为O(n * logn),排序性能比较高,但其缺点是当数组长度比较小时,归并的效率并不高且浪费空间;另一方面是如果两个序列的长度相差较大,一个长序列和一个序列进行归并,其归并带来的排序效率无法很好的体现。

算法步骤

步骤

TimSort排序算法的步骤如下:

  • 如果数组的长度小于MIN_MERGE=32时,直接使用二分插入排序。

  • 将待排序的数组划分成一个个子数组run,数组长度最小阈值为minRun,minRun的值根据数组长度进行计算。

  • 从数组第一位开始获取连续有序的run,为倒序时将数组翻转为正序。如果此时run的长度小于minRun,则使用二分插入算法从下一位进行补充。

  • 将上一步得到的run推入栈中,保存当次run的起始下标位置baseLen[]和run的长度runLen[]。判断栈中的run是否需要合并。当前栈中run数量大于1时进行循环

    • n = stackSize - 2
    • n = 0(run数量为2),runLen[n] <= runLen[n+1]时,将n与n+1的run进行合并(归并排序)
    • n > 0 且 runLen[n-1] <= runLen[n] + runLen[n+1]时,如果runLen[n - 1] < runLen[n + 1],n-1 与 n 合并,否则 n 与 n+1 合并
  • 移动起始下标,重复上述两步遍历到数组尾部。

  • 将栈中剩余的run进行最终合并,归并为一个正序的有序数组。

其中有几个关键点

MIN_MERGE

将要合并序列的大小的最小值。上面提到归并排序的缺点,当数组长度比较小时,归并的效率不高且浪费空间,所以直接采用二分插入排序。

minRun

划分的run对应的数组的最小阈值,其计算逻辑为

  • 当数组长度小于MIN_MERGE,直接返回数组长度,相当不做归并排序只进行插入排序,与上述对MIN_MERGE判断一致。
  • 如果数组长度是2的精准乘方(2^m,2的阶乘),返回MIN_MERGE/2
  • 其余返回整数k,MIN_MERGE/2 <= K <= MIN_MERGE,n/k接近但严格小于2的精准乘方。(严格小于即差值为1)
private static int minRunLength(int n) {
        assert n >= 0;
        int r = 0;      // Becomes 1 if any 1 bits are shifted off
        while (n >= MIN_MERGE) {   
            r |= (n & 1); // 奇数二进制低位为1,&1=1;偶数二进制低位为0,&1=0
            n >>= 1; // 向右移一位,相当于除以2
        }
        return n + r;
    }

上述代码为minRun的计算源码,计算逻辑为:当n>=32时,n除以2;当n不为2的阶乘时,r=1,否则r=0(n/2的过程中出现奇数),返回n+r。

由于TimSort在数组长度小于MIN_MERGE=32时直接使用二分插入排序,因为minRun的最小值为MIN_MERGE/2=16。

TimSort中并没有初始化一个堆栈,而是使用两个数据记录每个run的信息,run的起始下标位置信息baseLen[]、run的长度信息runLen[]。runBase[i] +runLen[i] = runBase[i+1]

在run合并的过程中是从后往前的进行合并的,合并后会将baseLen及runLen中的信息进行更新。

图示

TimSort算法的图示如下(为方便演示,假设minRun=4)

image.png

run合并

每当一个run入栈的时候,都会判断栈中的run是否需要进行合并。每次循环都以倒数第二个run做基准进行判断,判断逻辑在步骤中已整理。当判断需要合并时,就会进行一次归并排序,相比普通归并排序从下标0位开始,此时run对应的数组已经是有序的,对归并过程是友好的。

对run归并的处理逻辑在JDK1.8的java.util.TimSort.mergeAt()方法。简化的流程如下:

  • 找出run2的第一个元素在run1的位置(偏移个数),如比run1的第一个元素小则为0,获取run1对比起始下标
  • 找出run1的最后一个元素在run2的位置,获取run2移动对比长度
  • 从run1对比起始下标(dest)开始,将run1剩余元素拷贝到数组tmp中(下标cursor1)
  • 从run2起始下标(cursor2)开始循环与tmp[cursor1]对比,如a[cursor2]<tmp[cursor1],则a[dest]=a[cursor2],cursor2加1,dest加1;如a[cursor2]>=tmp[cursor1],则a[dest]=tmp[cursor1],cursor1加1,dest加1
  • 循环结束,如tmp中存在未遍历元素,将剩余元素从数组dest位置拷贝覆盖。

下图为上述TimSort算法演示图中第一次run合并的过程。

image.png

上图run2的第一个元素比run1的第一个元素小,则run1全部被拷贝进tmp,若将run1第一个元素修改为2(比4小),则如下图,run1的第一个元素2及run2的最后一个元素18是不需要比较移动的。这种处理方式能减少比较和移动次数,提高归并排序的性能,也是归并排序在待归并序列有序情况下的优势。

image.png

复杂度及稳定性

TimSort算法主流程的源码如下:

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

        // 数组长度小于MIN_MERGER,不进行归并处理
        if (nRemaining < MIN_MERGE) {
            // 从lo位置开始找出有序的序列,如果是倒序装换为正序
            int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
            // 二分插入排序,lo+iniRunLen为起始位置(因为这之前的都是有序且正序的)
            binarySort(a, lo, hi, lo + initRunLen, c);
            return;
        }

        /**
         *实例化TimSort对象
         * 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);
        // run最小长度
        int minRun = minRunLength(nRemaining);
        do {
            // Identify next run
            int runLen = countRunAndMakeAscending(a, lo, hi, c);

            // run长度小于minRun,使用插入法进行补充
            if (runLen < minRun) {
                int force = nRemaining <= minRun ? nRemaining : minRun;
                binarySort(a, lo, lo + force, lo + runLen, c);
                runLen = force;
            }

            // 将run推入栈中,并判断是否需要合并
            ts.pushRun(lo, runLen);
            ts.mergeCollapse();

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

        // 循环结束,将栈中剩余的序列合并
        assert lo == hi;
        ts.mergeForceCollapse();
        assert ts.stackSize == 1;
    }

从源码中可以看出,当待排序数组长度小于MIN_MERGER且已经是有序时,只进行一次查找有序序列的过程(countRunAndMakeAscending),是对数组的一次遍历,此时不需要进行二分插入排序也不需要进行归并。即最好情况下TimSort的时间复杂度为O(n)。

平均来看,使用了二分插入排序和归并排序,TimSort的时间复杂度为O(n*logn)。

在归并的过程使用了临时数组tmp存放待比对移动的元素,其空间复杂度为O(n)。

在二分插入即归并的过程中,对于大小相同的元素,没有改变其先后顺序,因此TimSort是一种稳定的排序算法。

快速排序的平均时间复杂度也为O(n*logn),但快速排序是一种不稳定的排序,无法保证排序前后相同大小元素的有序性,这在某些场景下会出现影响。

TimSort与ComparableTimSort

java.util包下还有一个类似的类ComparableTimSort,是对实现了Comparable接口的对象的TimSort排序应用,而不是显式地传进去一个Comparator对象,流程一致。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容