Java源碼之Arrays內部排序實現(timsort的實現)

在Arrays工作類裏有sort()方法可以用來排序,jdk對所有基本類型設置設置了不同入參sort方法進行支持。
這裏寫圖片描述
從源碼上看,基本類型的排序都是使用了了DualPivotQuicksort的排序方法(我看的是jdk8,)。DualPivotQuicksort是快排的一種優化,具體在這裏不展開了。
當參數類型爲對象數組時,在原來的版本使用的歸併排序(以後將會刪除 ),現在使用的timSort。

  public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a);
    }
    //以後會拋棄,也不展開了,大家可以自己去看下歸併排序
    /** To be removed in a future release. */
    private static void legacyMergeSort(Object[] a) {
        Object[] aux = a.clone();
        mergeSort(aux, a, 0, a.length, 0);
    }

所以排序主要用了 ComparableTimSort.sort(Object[] a)。分爲下面幾個主要步驟:

數組個數小於32的情況

  1. 判斷數組的大小,小於32使用二分插入排序
static void sort(Object[] a, int lo, int hi) {
        //檢查lo,hi的的準確性
        rangeCheck(a.length, lo, hi);
        int nRemaining  = hi - lo;

        //當長度爲0或1時永遠都是已經排序狀態
        if (nRemaining < 2)
            return;

        // 數組小的時候
        if (nRemaining < MIN_MERGE) {
            //找出連續升序的最大個數
            int initRunLen = countRunAndMakeAscending(a, lo, hi);
            //二分插入排序
            binarySort(a, lo, hi, lo + initRunLen);
            return;
        }

        //數組大於32的時
       ......
  1. 找出最大的遞增或者遞減的個數,如果遞減,則此段數組嚴格反一下方向
  private static int countRunAndMakeAscending(Object[] a, int lo, int hi) {
        int runHi = lo + 1;
        if (runHi == hi)
            return 1;

        // Find end of run, and reverse range if descending
        if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // 遞減
            while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)
                runHi++;
            //調整順序
            reverseRange(a, lo, runHi);
        } else {                              // 遞增
            while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0)
                runHi++;
        }

        return runHi - lo;
    }
  1. 使用在使用二分查找位置,進行插入排序。start之前爲全部遞增數組,從start+1開始進行插入,插入位置使用二分法查找。最後根據移動的個數使用不同的移動方法。
 private static void binarySort(Object[] a, int lo, int hi, int start) {

        if (start == lo)
            start++;
        for ( ; start < hi; start++) {

            Comparable<Object> pivot = (Comparable) a[start];

            int left = lo;
            int right = start;

            while (left < right) {
                int mid = (left + right) >>> 1;
                if (pivot.compareTo(a[mid]) < 0)
                    right = mid;
                else
                    left = mid + 1;
            }

            int n = start - left;  // 要移動的個數
            // 移動的方法
            switch (n) {
                case 2:  a[left + 2] = a[left + 1];
                case 1:  a[left + 1] = a[left];
                         break;
                //native複製數組方法
                default: System.arraycopy(a, left, a, left + 1, n);
            }
            a[left] = pivot;
        }
    }

數組個數大於32的情況

數組大於32時, 先算出一個合適的大小,在將輸入按其升序和降序特點進行了分區。排序的輸入的單位不是一個個單獨的數字,而是一個個的塊-分區。其中每一個分區叫一個run。針對這些 run 序列,每次拿一個run出來按規則進行合併。每次合併會將兩個run合併成一個 run。合併的結果保存到棧中。合併直到消耗掉所有的run,這時將棧上剩餘的 run合併到只剩一個 run 爲止。這時這個僅剩的 run 便是排好序的結果。

    static void sort(Object[] a, int lo, int hi) {
        //小於32
        ......
        //大於32的情況
        ComparableTimSort ts = new ComparableTimSort(a);

        //計算出run的長度
        int minRun = minRunLength(nRemaining);
        do {
            //找出連續升序的最大個數
            int runLen = countRunAndMakeAscending(a, lo, hi);

            // 如果run長度小於規定的minRun長度,先進行二分插入排序
            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();


            lo += runLen;
            nRemaining -= runLen;
        } while (nRemaining != 0);

        // 歸併所有的run
        ts.mergeForceCollapse();
    }

1.計算出run的最小的長度minRun

a) 如果數組大小爲2的N次冪,則返回16(MIN_MERGE / 2)

b) 其他情況下,逐位向右位移(即除以2),直到找到介於16和32間的一個數

 private static int minRunLength(int n) {
        int r = 0;      // Becomes 1 if any 1 bits are shifted off
        while (n >= MIN_MERGE) {
            r |= (n & 1);
            n >>= 1;
        }
        return n + r;
    }

2.求最小遞增的長度,如果長度小於minRun,使用插入排序補充到minRun的個數,操作和小於32的個數是一樣。
3.用stack記錄每個run的長度,當下面的條件其中一個成立時歸併,直到數量不變
runLen[i - 3] > runLen[i - 2] + runLen[i - 1]
runLen[i - 2] > runLen[i - 1]

 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 是 run1,run2 ,先用 gallopRight在 run1 裏使用 binarySearch 查找run2 首元素 的位置k, 那麼 run1 中 k 前面的元素就是合併後最小的那些元素。然後,在run2 中查找run1 尾元素 的位置 len2 ,那麼run2 中 len2 後面的那些元素就是合併後最大的那些元素。最後,根據len1 與len2 大小,調用mergeLo 或者 mergeHi 將剩餘元素合併。

 private void mergeAt(int i) {

        int base1 = runBase[i];
        int len1 = runLen[i];
        int base2 = runBase[i + 1];
        int len2 = runLen[i + 1];

        runLen[i] = len1 + len2;
        if (i == stackSize - 3) {
            runBase[i + 1] = runBase[i + 2];
            runLen[i + 1] = runLen[i + 2];
        }
        stackSize--;

        int k = gallopRight((Comparable<Object>) a[base2], a, base1, len1, 0);
        assert k >= 0;
        base1 += k;
        len1 -= k;
        if (len1 == 0)
            return;


        len2 = gallopLeft((Comparable<Object>) a[base1 + len1 - 1], a,
                base2, len2, len2 - 1);
        assert len2 >= 0;
        if (len2 == 0)
            return;

        if (len1 <= len2)
            mergeLo(base1, len1, base2, len2);
        else
            mergeHi(base1, len1, base2, len2);
    }

4.最後歸併還有沒有歸併的run,知道run的數量爲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,所以不合並,繼續找第二個run

  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]

參考:http://blog.csdn.net/bruce_6/article/details/38299199

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章