JDK1.8源码分析【排序】timsort

JDK1.8源码分析【排序】timsort

如无特殊说明,文中的代码均是JDK 1.8版本。

在JDK集合框架中描述过,JDK存储一组Object的集合框架是Collection。而针对Collection框架的一组操作集合体是Collections,里面包含了多种针对Collection的操作,例如:排序、查找、交换、反转、复制等。

这一节讲述Collections的排序操作。

public static extends Comparablesuper T>> void sort(List list) {
    list.sort(null);
}

Collections.sort方法调用的是List.sort方法,List.sort方法如下:

复制代码
    @SuppressWarnings({"unchecked", "rawtypes"})
    default void sort(Comparatorsuper E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);  // Arrays的排序方法
        ListIterator i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
复制代码

 

看到这里可能会觉得奇怪,List是接口,但为什么会有实现方法,这是JDK 1.8的新特性。具体特性描述请参考:Java 8接口有default method后是不是可以放弃抽象类了?

在List.sort方法实现中,排序使用的是Arrays#sort(T[], java.util.Comparator)方法,所以Collections的sort操作最终也是使用Arrays#sort(T[], java.util.Comparator)方法。

复制代码
    public static  void sort(T[] a, Comparatorsuper 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);
        }
    }
复制代码

Arrays#sort(T[], java.util.Comparator)方法使用了3种排序算法:

java.util.Arrays#legacyMergeSort 归并排序,但可能会在新版本中废弃
java.util.ComparableTimSort#sort 不使用自定义比较器的TimSort
java.util.TimSort#sort 使用自定义比较器的TimSort

 

 

 

 

Arrays源码中有这么一段定义:

复制代码
    /**
     * 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();
    }
复制代码

该定义描述是否使用LegacyMergeSort,即历史归并排序算法,默认为假,即不使用。所以Arrays.sort只会使用java.util.ComparableTimSort#排序或java.util.TimSort#排序,这两种方法的实现逻辑是一样的,只是java.util.TimSort#排序可以使用自定义的比较,而java.util.ComparableTimSort#排序不使用比较而已。

顺便补充一下,比较器是策略模式的一个完美又简洁的示例。总体来说,策略模式允许在程序执行时选择不同的算法。比如在排序时,传入不同的比较器(比较器),就采用不同的算法。

Timsort算法

Timsort是结合了合并排序(merge sort)和插入排序(插入排序)而得出的排序算法,它在现实中有很好的效率.Tim Peters在2002年设计了该算法并在Python中使用(TimSort)是Python中list.sort的默认实现)。该算法找到数据中已经排好序的块 - 分区,每一个分区叫一个run,然后按规则合并这些run.Pyhton自从2.3版以来一直采用Timsort算法排序, JDK 1.7开始也采用Timsort算法对数组排序。

Timsort的主要步骤:

判断数组的大小,小于32使用二分插入排序

复制代码
    static  void sort(Object [] a,int lo,int hi,Object [] work,int workBase,int workLen){
         // 检查lo,hi的的准确性
        断言 a!= null && lo> = 0 && lo < = hi && hi <= a.length;

        int nRemaining = hi  - lo;
        // 当长度为0或1时永远都是已经排序状态
        ifnRemaining <2 return ;  // 总是排序大小为0和1的数组

        // 数组个数小于32的时候
         // 如果阵列小,做一个“小TimSort”,没有合并
        如果(nRemaining < MIN_MERGE){
             // 找出连续升序的最大个数
            INT initRunLen = countRunAndMakeAscending(一,瞧,嗨);
            // 二分插入排序 
            binarySort(a,lo,hi,lo + initRunLen);
            回归;
        }

        // 数组个数大于32的时候 
       ......
复制代码

找出最大的递增或者递减的个数,如果递减,则此段数组严格反一下方向

复制代码
    private  static  int countRunAndMakeAscending(Object [] a,int lo,int hi){
         assert lo < hi;
        int runHi = lo + 1 ;
        if(runHi == hi)
             返回 1 ;

        // 查找运行结束,
        如果(((可比较)a [runHi ++])则降序反转范围 .compareTo(a [lo])<0){ // 降序递减
            (runHi 
                runHi ++ ;
            // 调整顺序
            reverseRange(a,lo,runHi);
        } else {                               // 升序递增
            (runHi  = 0 
                runHi ++ ;
        }

        return runHi  - lo;
    }
复制代码

在使用二分查找位置,进行插入排序。开始之前为全部递增数组,从开始+ 1开始进行插入,插入位置使用二分法查找。最后根据移动的个数使用不同的移动方法。

复制代码
    private  static  void binarySort(Object [] a,int lo,int hi,int start){
         assert lo <= start && start <= hi;
        if(start == lo)
            开始 ++ ;
        for(; start ){
            可比较的枢轴 = (可比较)a [开始];

            // 将左(和右)设置为[start](pivot)所属的索引
            int left = lo;
            int right = start;
            断言左<= 右;
            / *
             *不变量:
             * pivot> = all in [lo,left]。
             * pivot 
             * / 
            while(左< 右){
                 int mid =(左+右)>>> 1 ;
                if(pivot.compareTo(a [mid])<0  = 中;
                否则 =中+ 1 ;
            }
            断言左== 右;

            / *
             *不变量仍然保持:pivot> = all in [lo,left]和
             * pivot 注意
             *如果有等于枢轴的元素,则左边指向
             *他们之后的第一个位置 - 这就是为什么这种稳定。
             *将元件滑过以为枢轴腾出空间。
             * / 
            int n =开始 - 左;  // 要移动的元素数量要移动的个数
             // 在默认情况下,Switch只是arraycopy的优化
             // 移动的方法
            switch (n){
                 case 2:a [left + 2] = a [left + 1 ]。
                案例 1:a [left + 1] = a [left];
                         打破;
                // native复制数组方法
                默认值:System.arraycopy(a,left,a,left + 1 ,n);
            }
            a [left] = pivot;
        }
    }
复制代码

 

数组大小大于32时

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

复制代码
    static  void sort(Object [] a,int lo,int hi,Object [] work,int workBase,int workLen){
         // 数组个数小于32的时候
        ......

        // 数组个数大于32的时候
        / **
         *三月一次,从左到右,找到自然运行,
         *将短自然运行扩展到minRun元素,并合并运行
         *保持堆栈不变。
         * / 
        ComparableTimSort ts = new ComparableTimSort(a,work,workBase,workLen);
        // 计算run的长度
        int minRun = minRunLength(nRemaining);
        do {
             // 识别下一次运行
             // 找出连续升序的最大个数
            int runLen = countRunAndMakeAscending(a,lo,hi);

            // 如果run是short,则延伸到min(minRun,nRemaining)
             // 如果运行长度小于规定的minRun长度,先进行二分插入排序
            if(runLen < minRun){
                 int force = nRemaining <= minRun?nRemaining:minRun;
                binarySort(a,lo,lo + force,lo + runLen);
                runLen = force;
            }

            //将run运行到pending-run stack,并且可能合并
            ts.pushRun(lo,runLen);
            // 进行归并
            ts.mergeCollapse();

            // 提前找到下一个运行 
            lo + = runLen;
            nRemaining - = runLen;
        } while(nRemaining!= 0 );

        // 合并所有剩余的运行以完成排序
        断言 lo == hi;
        // 归并所有的run 
        ts.mergeForceCollapse();
        断言 ts.stackSize == 1 ;
    }
复制代码

1.计算出跑的最小的长度minRun

  a)如果数组大小为2的N次幂,则返回16(MIN_MERGE / 2);

  b)其他情况下,逐位向右位移(即除以2),直到找到介于16和32间的一个数;

复制代码
    / **
     *返回指定数组的最小可接受运行长度
     * 长度。比这更短的自然运行将延长
     * { @link #binarySort}。
     *
     *粗略地说,计算是:
     *
     *如果n 
     *否则如果n的精确幂为2,则返回MIN_MERGE / 2。
     *否则返回一个int k,MIN_MERGE / 2 <= k <= MIN_MERGE,这样n / k
     *接近但严格小于2的精确幂。
     *
     *有关基本原理,请参阅listsort.txt。
     *
     * @param n要排序的数组的长度
     * @return 要合并的最小运行的长度
      * / 
    private  static  int minRunLength(int n){
         assert n> = 0 ;
        int r = 0;      // 如果任何1位被移开,
        变为1(n> = MIN_MERGE){
            r | =(n&1 );
            n >> = 1 ;
        }
        返回 n + r;
    }
复制代码

2.求最小递增的长度,如果长度小于minRun,使用插入排序补充到minRun的个数,操作和小于32的个数是一样 
.3。用栈记录每个run的长度,当下面的条件其中一个成立时归并,直到数量不变:

runLen [i  -  3]> runLen [i  -  2] + runLen [i  -  1 ]
runLen [i -  2]> runLen [i  -  1]
复制代码
    / **
     *检查等待合并的运行堆栈并合并相邻的运行
     *直到重新建立堆栈不变量:
     *
     * 1. runLen [i  -  3]> runLen [i  -  2] + runLen [i  -  1]
     * 2. runLen [i  -  2]> runLen [i  -  1]
     *
     *每次将新运行推入堆栈时都会调用此方法,
     *所以不变量保证为i 
     *进入该方法。
     * / 
    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] ])
                    n - ;
                mergeAt(N);
            } else  if(runLen [n] <= runLen [n + 1 ]){
                mergeAt(N);
            } else {
                 break ; // 建立不变量
            }
        }
    }
复制代码

关于归并方法和对一般的归并排序做出了简单的优化。假设两个运行是run1,run2,先用gallopRight在run1里使用binarySearch查找run2首元素的位置k,那么run1中k前面的元素就是合并最后,根据len1与len2大小,调用mergeLo或者mergeHi将剩余元素合并。

复制代码
    / **
     *在堆栈索引i和i + 1处合并两次运行。我必须跑
     *倒数第二个或倒数第二个在堆栈上运行。换一种说法,
     *我必须等于stackSize-2或stackSize-3。
     *
     * @param 我将两个运行中的第一个的索引堆叠为合并
      * / 
    @SuppressWarnings( “unchecked” private  void mergeAt(int i){
         assert stackSize> = 2 ;
        断言 i> = 0 ;
        断言 i == stackSize  -  2 || i == stackSize  -  3 ;

        int base1 = runBase [i];
        int len1 = runLen [i];
        int base2 = runBase [i + 1 ];
        int len2 = runLen [i + 1 ];
        断言 len1> 0 && len2> 0 ;
        断言 base1 + len1 == base2;

        / *
         *记录组合运行的长度; 如果我是第三名
         *现在运行,也可以在最后一次运行时滑动(不涉及
         *在此合并中)。无论如何,当前的运行(i + 1)都会消失。
         * / 
        runLen [i] = len1 + len2;
        if(i == stackSize  -  3 ){
            runBase [i + 1] = runBase [i + 2 ];
            runLen [i + 1] = runLen [i + 2 ];
        }
        stackSize - ;

        / *
         *找到run2的第一个元素在run1中的位置。先前的要素
         *在run1中可以忽略(因为它们已经到位)。
         * / 
        int k = gallopRight((Comparable )a [base2],a,base1,len1,0 );
        断言 k> = 0 ;
        base1 + = k;
        len1 -  = k;
        if(len1 == 0 返回;

        / *
         *找到run1的最后一个元素在run2中的位置。后续元素
         *在run2中可以忽略(因为它们已经到位)。
         * / 
        len2 = gallopLeft((Comparable )a [base1 + len1  -  1 ],a,
                base2,len2,len2 -  1 );
        断言 len2> = 0 ;
        if(len2 == 0 返回;

        // 合并剩余的运行,使用带有min(len1,len2)元素的tmp数组
        if(len1 <= len2)
            mergeLo(base1,len1,base2,len2);
        其他
            mergeHi(base1,len1,base2,len2);
    } 
          
复制代码

4.最后归并还有没有归并的跑,知道跑的数量为1。

例子

为了演示方便,我将TimSort中的minRun直接设置为2,否则我不能用很小的数组演示。同时把MIN_MERGE也改成2(默认为32),这样避免直接进入二分插入排序。

 

1.初始数组为[7,5,1,2,6,8,10,12,4,3,9,11,13,15,16,14]

2.寻找第一个连续的降序或升序序列:[1,5,7] [2,6,8,10,12,4,3,9,11,13,15,16,14]

3. stackSize = 1,所以不合并,继续找第二个运行

4.找到一个递减序列,调整次序:[1,5,7] [2,6,8,10,12] [4,3,9,11,13,15,16,14]

5.因为runLen [0] <= runLen [1]所以归并 

  1)gallopRight:寻找run1的第一个元素应当插入run0中哪个位置(“2”应当插入“1”之后),然后就可以忽略之前run0的元素(都比run1的第一个元素小) 

  2)gallopLeft:寻找run0的最后一个元素应当插入run1中哪个位置(“7”应当插入“8”之前),然后就可以忽略之后run1的元素(都比run0的最后一个元素大) 

  这样需要排序的元素就仅剩下[5,7] [2,6],然后进行mergeLow完成之后的结果:[1,2,5,6,7,8,10,12] [4,3, 9,11,13,15,16,14]

6.寻找连续的降序或升序序列[1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16,14]

7.不进行归并排序,因为runLen [0]> runLen [1]

8.寻找连续的降序或升序序列:[1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16] [14]

9.因为runLen [1] <= runLen [2],所以需要归并

10.使用gallopRight,发现为正常顺序。得[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14]

11.最后只剩下[14]这个元素:[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14]

12.因为runLen [0] <= runLen [1] + runLen [2]所以合并。因为runLen [0]> runLen [2],所以将run1和run2先合并。(否则将run0和run1先合并) 
  完成之后的结果:[1,2,5,6,7,8,10,12] [3,4,9,11,13,14,15,16]

13.完成之后的结果:[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]

 

你可能感兴趣的:(JDK1.8源码分析【排序】timsort)