Arrays.Sort()中的那些排序算法

本文基於JDK 1.8.0_211撰寫,基於java.util.Arrays.sort()方法淺談目前Java所用到的排序算法,僅我的看法和筆記,如有問題歡迎指證,着重介紹其中的TimSort排序,其源於Python,並於JDK1.7引入Java以替代原有的歸併排序。java

引入

Arrays.Sort方法所用的排序算法主要涉及如下三種:雙軸快速排序(DualPivotQuicksort)、歸併排序(MergeSort)、TimSort,也同時包含了一些非基於比較的排序算法:例如計數排序。其具體最終使用哪種排序算法一般根據類型以及輸入長度來動態抉擇python

  • 輸入數組類型爲基礎類型時,採用雙軸快速排序,輔以計數排序;
public static void sort(int[] a) {
    DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
  • 輸入數組類型爲包裝類型時,採用歸併排序或TimSort(除非通過特殊的配置,不然默認採用TimSort)
public static void sort(Object[] a) {
    if (LegacyMergeSort.userRequested)
      legacyMergeSort(a);
  else
    ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
 
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);
    }
}
 
/** To be removed in a future release. */
 private static <T> void legacyMergeSort(T[] a, Comparator<? super T> c) {
  T[] aux = a.clone();
  if (c==null)
      mergeSort(aux, a, 0, a.length, 0);
  else
    mergeSort(aux, a, 0, a.length, 0, c);
  }

排序穩定性

假定在待排序的記錄序列中,存在多個具備相同的關鍵字的記錄,若通過排序,這些記錄的相對次序保持不變,即在原序列中,r[i] = r[j],且 r[i] 在 r[j] 以前,而在排序後的序列中,r[i] 仍在 r[j] 以前,則稱這種排序算法是穩定的;不然稱爲不穩定的。android

穩定性的含義:若是咱們須要在二次排序後保持原有排序的意義,就須要使用到穩定性的算法git

例子:咱們要對一組商品排序,商品存在兩個屬性:價格和銷量。當咱們按照價格從高到低排序後,要再按照銷量對其排序,這時,若是要保證銷量相同的商品仍保持價格從高到低的順序,就必須使用穩定性算法。github

快速排序與雙軸快速排序

快速排序簡介

單軸快速排序 即爲咱們最多見的一種快速排序方式,咱們選取一個基準值(pivot),將待排序數組劃分爲兩部分:大於pivot 和 小於pivot,再對這兩部分進行單軸快速排序... 可是這種方式對於輸入數組中有不少重複元素時效率不太理想。算法

單軸三取樣切分快速排序 做爲單軸快速排序的優化版本,其主要優化相同元素過多狀況下的快排效率,一樣選取一個基準值,但將待排序數組劃分三部分: 大於pivot、等於pivot、大於pivot。api

雙軸快速排序 選取兩個 基準值pivot1,pivot2,且保證pivot1<=pivot2,其具體實現與三取樣切分相似,不過期將待排序數組劃分如下三部分:小於pivot,介於pivot1與pivot2之間,大於pivot2。數組

DualPivotQuicksort實現優化

Java在實現DualPivotQuickSort時,並未盲目使用雙軸快速排序,而是基於輸入長度選取排序算法app

(1)針對int、long、float、double四種類型,跟據長度選取的排序算法以下:svn

  • 當待排序數目小於47,採用插入排序
  • 當待排序數目小於286,採用雙軸快排
  • 當待排序數目大於286,採用歸併排序

咱們暫且將其稱之爲一個標準的雙軸快排

 static void sort(int[] a, int left, int right,
                     int[] work, int workBase, int workLen) {
        // Use Quicksort on small arrays
        if (right - left < QUICKSORT_THRESHOLD) {
            sort(a, left, right, true);
            return;
        }
        // Use MergingSort
       doMerge();
 }

private static void sort(int[] a, int left, int right, boolean leftmost) {
      // Use insertion sort on tiny arrays
      if (length < INSERTION_SORT_THRESHOLD) {
            doInertionSort();
      }
      doDualPivotQuickSort();
}            

 

(2)針對short、char兩種類型,根據長度選取的排序算法以下:

  • 當待排序數目小於3200,採用標準雙軸快排
  • 當待排序數目大於3200,採用計數排序(CountingSort)

(3)針對byte類型,根據長度選取的排序算法以下:

  • 當待排序數目小於29,採用插入排序
  • 當待排序數目大於29,採用計數排序(CountingSort)

非基於比較排序算法-計數排序

計數排序與傳統的基於比較的排序算法不一樣,其不經過比較來排序。咱們常見的非基於比較的排序算法有三種:桶排序、計數排序(特殊桶排序)、基數排序,有興趣的能夠逐個去了解,這裏主要講解計數排序。

使用前提

使用計數排序待排序內容須要是一個有界的序列,且可用整數表示

引入

計數排序顧名思義即須要統計數組中的元素個數,經過另一個數組的地址表示輸入元素的值,數組的值表示元素個數。最後再將這個數組反向輸出便可完成排序,見下方示例:

假設:一組範圍在 0~10 之間的數字,9, 3, 5, 4, 9, 1, 2, 7, 8,1,3, 6, 5, 3, 4, 0, 10, 9, 7, 9

第一步:創建一個數組來統計每一個元素出現的個數,由於是0~10,則創建以下數組 Count

 

 

 

第二步:遍歷輸入數組,將獲取到的內容放置到Count 數組對應的位置,使當前位置+1,例如當前爲9:

以此類推,遍歷完整個輸入數組,則獲得一個完整狀態的Count:

這時候,Count中記錄了輸入數組全部的元素出現的次數,將Count數組按次數輸出便可獲得最終排序輸出:

0, 1, 1, 2, 3, 3, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 9, 9, 9, 10

計數排序的穩定性與最終實現

根據上面穩定性的定義,你們不難發現上面的實現過程不能保證基數排序的穩定性,而實際上計數排序是一個穩定的排序算法,下面咱們經過上面那個例子來引出計數排序的最終實現。

例子:輸入數組  [ 0,9,5,4,5 ],範圍0 ~ 9

第一步:將Count數組中所記錄的內容進行更改,由記錄當前元素的出現的次數 修正爲  記錄當前索引出現的次數和當前索引以前全部元素次數的和,這樣在索引中存儲的值就是其真實的排序後排位數,例如9的值爲5,則9的排序後的位置就是第5位:

第二步:咱們從後向前遍歷原序列,此時爲5,則在最終排序序列的位置爲4(索引爲3)的地方放置5,並將Count序列的5索引處的值-1。

以此類推:到第二個5時,Count數組中的值爲3(索引爲2),這樣就保證了插入排序的穩定性。

 

 

源碼實現

 

 /** The number of distinct byte values. */
private static final int NUM_BYTE_VALUES = 1 << 8;

static void sort(byte[] a, int left, int right) {
    int[] count = new int[NUM_BYTE_VALUES];
    
    // 創建count數組
    for (int i = left - 1; ++i <= right;
        count[a[i] - Byte.MIN_VALUE]++
    );

    // 從尾部開始遍歷
    for (int i = NUM_BYTE_VALUES, k = right + 1; k > left; ) {
while (count[--i] == 0); byte value = (byte) (i + Byte.MIN_VALUE); int s = count[i]; do { a[--k] = value; } while (--s > 0); } }

TimSort

Timsort是工業級算法,其混用插入排序與歸併排序,二分搜索等算法,亮點是充分利用待排序數據可能部分有序的事實,而且依據待排序數據內容動態改變排序策略——選擇性進行歸併以及galloping。另外TimSort是一個穩定的算法,其最好的執行狀況要優於普通歸併排序,最壞狀況與歸併排序相仿:

針對小數據優化

針對輸入長度較短的數組排序,不少輕量級排序便可勝任,故TimSort在基於輸入數組長度的條件下,作出以下優化:

當輸入序列的長度小於基準值時,將採用插入排序直接對輸入序列排序。基準值的選取:(1)Python:64(2)Java:32

此外上面提到的插入排序,Java的實現中,對這部分作了優化,嚴格來講這裏使用的是二分插入排序。將插入排序與二分查找進行告終合。由於插入排序的索引左側均是有序序列

  • 傳統意義上須要單個依次向前比較,直到找到新插入元素放置的位置;
  • 二分插入排序,則在此處採用二分查找來確認插入元素的放置位置。

TimSort簡要流程

TimSort is a hybrid sorting algorithm that uses insertion sort and merge sort.

The algorithm reorders the input array from left to right by finding consecutive (disjoint) sorted segments (called 「runs」 from hereon). If the run is too short, it is extended using insertion sort.  The lengths of the generated runs are added to an array named runLen. Whenever a new run is added to runLen, a method named mergeCollapse merges runs until the last 3 elements in runLen satisfy the following two conditions (the 「invariant」):

  1. runLen [n-2] > runLen [n-1] + runLen [n]
  2. runLen [n-1] > runLen [n] 

Here n is the index of the last run in runLen.  The intention is that checking this invariant on the top 3 runs in runLen in fact guarantees that all runs satisfy it. At the very end, all runs are merged, yielding a sorted version of the input array.

TimSort算法經過找到連續的(不相交)排序的段(此後稱爲「Run」),若是Run太短,則使用插入排序擴充。生成的Run的長度被稱添加到一個名爲runLen數組中,每當將新Run添加到runLen時,名爲mergeCollapse的方法就會嘗試合併Run,直到runLen中的元素元素知足兩個恆定的不等式。到最後,全部Run都將合併成一個Run,從而生成輸入數組的排序版本。

基本概念 - Run

TimSort算法中,將有序子序列(升序序列嚴格降序序列)稱之爲Run,例如以下將排序序列:1,2,3,4,5,4,3,6,8,10

則上面的序列全部的Run以下:1,2,3,4,5,5,3,2,2,6,8,10

TimSort中會區分其序列爲升序仍是降序,若是是降序會強行反轉Run,使其成爲升序,則上述的序列通過反轉後即爲:1,2,3,4,5,5,2,3,2,6,8,10

注意:Run必須是升序(能夠相等)和嚴格降序(不能相等),嚴格降序的緣由是保證TimSort穩定性,由於降序須要反轉

基本概念 - MinRun

當兩個數組歸併時,當這個數組Run的數目等於或略小於2的乘方時,效率最高(基本數學概念參考:listsort.txt)

反過來講,咱們須要獲得一個Run的最小長度使得其劃分的Run的數目達到上述標準準,即:選取32-64(16-32)這個範圍做爲MinRun的範圍,使得原排序數組能夠被MinRun分割成N份,這個N等於或略小於2的乘方

  • 若是當前的Run長度小於MinRun:嘗試把後面的元素經過插入排序放入Run中,以儘可能達到MinRun的長度(剩餘長度知足的狀況下);
  • 若是當前的Run長度大於MinRun:不處理。

經過上述的操做咱們就收穫了一系列Run,將其放置到堆棧runLen中,並嘗試對其進行歸併:

 

    /**
     * A stack of pending runs yet to be merged. Run i starts at
     * address base[i] and extends for len[i] elements. It's always
     * true (so long as the indices are in bounds) that:
     *
     * runBase[i] + runLen[i] == runBase[i + 1]
     *
     * so we could cut the storage for this, but it's a minor amount,
     * and keeping all the info explicit simplifies the code.
     */
    private int stackSize = 0; // Number of pending runs on stack
    private final int[] runBase;
    private final int[] runLen;

     /*
      * 初始化部分截取
      * 分配runs-to-be-merged堆棧(不能擴大)
      */
    int stackLen = (len < 120 ? 5 : len < 1542 ? 10 : len < 119151 ? 24 : 49);
    runBase new int[stackLen];
    runLen new int[stackLen];

    /**
      * Pushes the specified run onto the pending-run stack.
      *
      * @param runBase index of the first element in the run
      * @param runLen  the number of elements in the run
      */
    private void pushRun(int runBase, int runLen) {
        this.runBase[stackSize] = runBase;
         this.runLen[stackSize] = runLen;
         stackSize++;
    }

歸併原則 - 不等式

基於以下緣由:

(1)堆棧runLen內存佔用儘可能減少,則其是線上具備固定大小,不考慮擴容(參考上述源碼的註釋);

(2)讓歸併的兩個Run的數目都儘可能接近,更接近普通歸併的模式,提升歸併效率。

TimSort在runLen上制定了兩個不等式以使runLen儘可能貼近上面的條件:

  • Run[n-1] > Run[n] + Run[n+1]
  • Run[n] > Run[n+1]

當目前runLen知足這兩個不等式時,則不進行歸併,不然進行歸併,由於TimSort時穩定的排序算法,則僅容許歸併相鄰的兩個Run:

  • 若是不知足不等式一:歸併Run[n]與Run[n-1]、Run[n+1]中長度較短的Run
  • 若是不知足不等式二:歸併Run[n]與Run[n+1]

不變式的含義:

不變式一:從右至左讀取長度,則待處理的runLen的增加至少與斐波那契額增加的速度同樣快,則更使得兩個歸併的Run的大小更爲接近;

不變式二:待處理的runLen按遞減順序排序。

經過以上兩個推論能夠推測runLen中的Run數目永遠會收斂於一個肯定的數,以此證實了只用極小的runLen堆棧就能夠排序很長的輸入數組,也正是由於如此在實現上不考慮擴容問題,若是須要詳細數學例證能夠查看文後Reference。

實際代碼是線上,Java、Python、Android保證不等式的手段是檢查棧頂三個元素是否知足,即上述不等式的n取棧頂第二個,若是不知足則歸併,歸併完成後再繼續檢查棧頂三個直到知足爲止

 /**
     * 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
            }
        }
    }

歸併排序

歸併優化一:內存優化

因爲須要保證TimSort的穩定性,則歸併排序不能採用原地排序,TimSort引入了臨時數組來進行歸併,並將參與歸併的兩個Run中較小的那個放置到臨時數組中,以節省內存佔用。同時區分從小開始歸併和從大開始歸併。

歸併優化二:縮短歸併Run長度

兩個參與歸併的Run,由不少部分其實時不用參與歸併的(從Run的某個位置開始的前面或後面的元素均小於或大於另外一個Run的所有元素,則這部分就能夠不參與歸併),加之歸併兩個Run是在原輸入數組中是相鄰,則咱們考慮是否能夠在去掉部分頭尾元素,以達到縮短歸併長度的目的:

(1)在RunA查找一個位置 I 能夠正好放置RunB[0],則 I 以前的數據都不用參與歸併,在原地不須要變化;

(2)在RunB查找一個位置 J 能夠正好放置RunA[Len-1],則 J 以後的數據都不用參與歸併,在原地不須要變化;

歸併排序優化三:GallopingMode

在歸併時有可能存在如下的極端狀況:

RunA 的全部元素都小於RunB,若是這個狀況下采用常規的歸併效率確定不理想

因而TimSort引入了GallopingMode,用來解決上述的問題,即當歸並時,一個Run連續n的元素都小於另外一個Run,則考慮是否有更多的元素都小於,則跳出正常歸併,進入GallopingMode,當不知足Galloping條件時,再跳回到正常歸併(不知足Galloping條件時強制使用Galloping效率低下)。若是RunA的許多元素都小於RunB,那麼有可能RunA會繼續擁有小於RunB的值(反之亦然),這個時候TimSort會跳出常規的歸併排序進入Galloping Mode,這裏規定了一個閾值MIN_GALLOP,默認值爲7

下面模擬一次Galloping,以mergeHi爲例(從大開始歸併):

(1)例如此時RunA已經連續贏得7次歸併,而RunB的元素尚未一次被選取,則已經達到閾值,進入GallopingMode:

進入GallopingMode,說明此時已經有RunA已經小於RunB末尾的7個數字,TimSort猜想會有更多的RunA的數字小於RunB,則進行如下操做:

 

以上則完成了一次Galloping,在這一次Galloping中,咱們一次性將全部大於RunB[len-1]的RunA元素一次性移動(包括RunB[len-1],放置到此次移動的RunA元素後),不須要再依次歸併。

這時涉及到一個概念便是否繼續執行Galloping,仍是回到正常的歸併?

咱們判斷這一次移動的元素個數是否還知足閾值(黃色),若是知足則繼續,在RunA中尋找RunB[len-2]的位置,不然回到正常的歸併。

Java版本實現中每次進入和退出Galloping會變動這個進入GallopingMode的閾值,應該是某種獎懲機制,這裏不作說明。

TimSort 的實現缺陷

在Java8中TimSort的實現是有缺陷的,在極端複雜狀況下可能會觸發異常,其主要緣由是若是隻檢查棧頂三個Run的不等式關係,沒辦法辦證這個不等式在整個runLen堆棧上成立,參考如下示例:

不等式被破壞的代價,即爲Run的歸併時機被破壞,致使在某些極端狀況下,會致使堆棧中的Run超過咱們經過不等式推出來的那個收斂值致使溢出:ArrayOutOfBoundsException,這個Bug在後續版本已經修復:

提供的修復方案即爲檢查棧頂四個Run而非三個:

private void newMergeCollapse() {
     while (stackSize > 1) {
       int n = stackSize - 2;
       if (   (n >= 1 && runLen[n-1] <= runLen[n] + runLen[n+1])
           || (n >= 2 && runLen[n-2] <= runLen[n] + runLen[n-1])) {
                if (runLen[n - 1] < runLen[n + 1])
                    n--;
            } else if (runLen[n] > runLen[n + 1]) {
                break; // Invariant is established
            }
            mergeAt(n);
        }
    }

有興趣手動Debug這個問題的讀者能夠參考以下Git操做:https://github.com/Rekent/java-timsort-bug

部分源碼註釋

入口方法:

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
        }

        // 若是小於32位,則不採用TimSort,而是採用二分插入排序對整個數組直接排序
        if (nRemaining < MIN_MERGE) {
            // 獲取第一個序列(Run)
            int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
            // 二分插入排序(從第一個Run後開始向前排序,第一個Run已是增序了,不用再排)
            binarySort(a, lo, hi, lo + initRunLen, c);
            return;
        }

        MyTimSort<T> ts = new MyTimSort<>(a, c, work, workBase, workLen);
        // 根據當前排序數組長度生成最小的Run長度
        int minRun = minRunLength(nRemaining);
        do {
            // 識別下一個Run序列,返回這個Run的長度
            int runLen = countRunAndMakeAscending(a, lo, hi, c);

            // 若是當前的Run序列長度小於MinRun長度,則嘗試擴展到MinRun的長度(從後面選取元素進行二分拆入排序)
            if (runLen < minRun) {
                // 若是剩餘長度小於當前的MinRun,則補全爲剩餘的長度
                int force = nRemaining <= minRun ? nRemaining : minRun;
                binarySort(a, lo, lo + force, lo + runLen, c);
                runLen = force;
            }

            // 記錄當前Run的起始Index以及Run長度
            ts.pushRun(lo, runLen);
            // 嘗試合併
            ts.mergeCollapse();

            // 開始下一輪的Run尋找
            lo += runLen;
            nRemaining -= runLen;
        } while (nRemaining != 0);

        // 全部的Run都已經尋找完畢,必須合併全部Run
        assert lo == hi;
        ts.mergeForceCollapse();
        assert ts.stackSize == 1;
    }

歸併方法(優化縮短歸併長度):

/**
     * 合併i以及i+1兩個Run. Run i 必定是倒數第二個或者倒數第三個Run
     *
     * @param i 合併堆棧的索引
     */
    private void mergeAt(int i) {
        // i 必定是倒數第二個或者倒數第三個,換句話說,i必定是stackSize-2或者stackSize-3
        assert stackSize >= 2;
        assert i >= 0;
        assert i == stackSize - 2 || i == stackSize - 3;

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

        // 將i的長度更新len1+len2,即合併後的長度,若是是倒數第三個Run,則將倒數第一個長度合併到倒數第二個Run中(倒數第二個和倒數第三個Run合併了)
        // [RUN1,RUN2,RUN3] -> [RUN1+RUN2,RUN3] ; [RUN1,RUN2] -> [RUN1+RUN2]
        runLen[i] = len1 + len2;
        if (i == stackSize - 3) {
            runBase[i + 1] = runBase[i + 2];
            runLen[i + 1] = runLen[i + 2];
        }
        stackSize--;

        // 縮短歸併長度:在Run1中尋找Run2的起始節點位置(Run2的首個元素應該放置Run1中的位置),能夠忽略run1中先前的元素(由於其已經有序)
        int k = gallopRight(a[base2], a, base1, len1, 0, c);
        assert k >= 0;
        // !!! Run1 這個位置以前的能夠省略再也不排序,由於Run2全部元素都大於這個位置
        base1 += k;
        len1 -= k;
        // 若是剩餘排序長度爲0,則已經有序,不用再排序(Run1全體大於Run2)
        if (len1 == 0) {
            return;
        }

        // 縮短歸併長度:在Run2中尋找Run1的最後一個元素應該放置的位置,!!! Run2的這個位置後面能夠省略再也不排序,由於後面全部元素都大於Run1
        len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
        assert len2 >= 0;
        // 若是剩餘排序長度爲0,則已經有序,不用再排序(Run2全體大於Run1)
        if (len2 == 0) {
            return;
        }

        // 並歸兩側,選取較短的一方做爲臨時數組長度
        // mergeLo:將len1 放入臨時數組,mergeHi:將len2 放入臨時數組
        if (len1 <= len2) {
            mergeLo(base1, len1, base2, len2);
        } else {
            mergeHi(base1, len1, base2, len2);
        }
    }

歸併與Gollaping:

/**
     * 相似於mergeLo,除了這個方法應該只在len1 >= len2;若是len1 <= len2,則應該調用mergeLo。
     * (兩個方法均可以在len1 == len2時調用。)
     *
     * @param base1 Run1的隊首元素
     * @param len1  Run1的長度
     * @param base2 Run2的隊首元素(???must be aBase + aLen)
     * @param len2  Run2的長度
     */
    private void mergeHi(int base1, int len1, int base2, int len2) {
        assert len1 > 0 && len2 > 0 && base1 + len1 == base2;

        T[] a = this.a; // For performance
        T[] tmp = ensureCapacity(len2);
        int tmpBase = this.tmpBase;
        System.arraycopy(a, base2, tmp, tmpBase, len2);

        int cursor1 = base1 + len1 - 1; 
        int cursor2 = tmpBase + len2 - 1; 
        int dest = base2 + len2 - 1; 

        // Move last element of first run and deal with degenerate cases
        a[dest--] = a[cursor1--];
        if (--len1 == 0) {
            System.arraycopy(tmp, tmpBase, a, dest - (len2 - 1), len2);
            return;
        }
        // 簡化操做,若是Run2要合併的元素只有一個,這個元素不比Run1的最大值大,也不比當前Run1索引的最小值大(base1的位置是大於Run2隊首元素的位置),故Run2這個元素應該放置到Run1第一個
        if (len2 == 1) {
            dest -= len1; // dest = dest - len1 (因前序dest已經-1,故是Run1隊尾座標),dest:Run1應該開始合併的起始位置:[....{1},6,16,0,27]
            cursor1 -= len1; // cursor1 = cursor1 - len1 (因前序cursor1已經-1,故是Run1倒數第二座標),cursor1:dest的前序位置:[...{X},1,6,16,0,27]
            System.arraycopy(a, cursor1 + 1, a, dest + 1, len1); // [...{1,6,16},0,27] -> [...1,{1,6,16},27],將Run要排序部分後移一位
            a[dest] = tmp[cursor2]; // [....1,1,6,16,27] -> [...0,1,6,16,27],將tmp中的惟一元素放置到隊首
            return;
        }

        Comparator<? super T> c = this.c; // Use local variable for performance
        int minGallop = this.minGallop; // " " " " "
        outer: while (true) {
            // 開始正常歸併,並記錄Run一、Run2 贏得選擇的次數,若是大於minGallop則跳出進入GallopingMode
            int count1 = 0; // Number of times in a row that first run won
            int count2 = 0; // Number of times in a row that second run won

            /*
             * Do the straightforward thing until (if ever) one run
             * appears to win consistently.
             */
            do {
                assert len1 > 0 && len2 > 1;
                if (c.compare(tmp[cursor2], a[cursor1]) < 0) {
                    a[dest--] = a[cursor1--];
                    count1++;
                    count2 = 0;
                    if (--len1 == 0) {
                        break outer;
                    }
                } else {
                    a[dest--] = tmp[cursor2--];
                    count2++;
                    count1 = 0;
                    if (--len2 == 1) {
                        break outer;
                    }
                }
            } while ((count1 | count2) < minGallop);

            /*
             * 一方已經連續合併超過minGallop次,則進入GallopingMode
             */
            do {
                assert len1 > 0 && len2 > 1;
                count1 = len1 - gallopRight(tmp[cursor2], a, base1, len1, len1 - 1, c);
                if (count1 != 0) {
                    dest -= count1;
                    cursor1 -= count1;
                    len1 -= count1;
                    System.arraycopy(a, cursor1 + 1, a, dest + 1, count1);
                    if (len1 == 0) {
                        break outer;
                    }
                }
                a[dest--] = tmp[cursor2--];
                if (--len2 == 1) {
                    break outer;
                }

                count2 = len2 - gallopLeft(a[cursor1], tmp, tmpBase, len2, len2 - 1, c);
                if (count2 != 0) {
                    dest -= count2;
                    cursor2 -= count2;
                    len2 -= count2;
                    System.arraycopy(tmp, cursor2 + 1, a, dest + 1, count2);
                    if (len2 <= 1) {
                        break outer;
                    }
                }
                a[dest--] = a[cursor1--];
                if (--len1 == 0) {
                    break outer;
                }
                // 完成一次Galloping後對閾值作修改,並判斷是否須要繼續Galloping
                minGallop--;
            } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
            if (minGallop < 0) {
                minGallop = 0;
            }
            minGallop += 2; // Penalize for leaving gallop mode
        } // End of "outer" loop
        this.minGallop = minGallop < 1 ? 1 : minGallop; // Write back to field

        if (len2 == 1) {
            assert len1 > 0;
            dest -= len1;
            cursor1 -= len1;
            System.arraycopy(a, cursor1 + 1, a, dest + 1, len1);
            a[dest] = tmp[cursor2]; // Move first elt of run2 to front of merge
        } else if (len2 == 0) {
            throw new IllegalArgumentException("Comparison method violates its general contract!");
        } else {
            assert len1 == 0;
            assert len2 > 0;
            System.arraycopy(tmp, tmpBase, a, dest - (len2 - 1), len2);
        }
    }

Reference

http://www.envisage-project.eu/proving-android-java-and-python-sorting-algorithm-is-broken-and-how-to-fix-it/

https://svn.python.org/projects/python/trunk/Objects/listsort.txt

相關文章
相關標籤/搜索