MergeSort與TimSort,ComparableTimSort

MergeSort歸并排序對已經反向排好序的輸入時復雜度為O(n^2),而TimSort就是針對這種情況,對MergeSort進行優化而產生的,平均復雜度為nO(log n),最好的情況為O(n),最壞情況nO(log n)。并且TimSort是一種穩定性排序。思想是先對待排序列進行分區,然后再對分區進行合并,看起來和MergeSort步驟一樣,但是其中有一些針對反向和大規模數據的優化處理。

通過一個例子來說:ArrayList中的sort(),調用了Arrays.sort()

    @Override
    @SuppressWarnings("unchecked")
    public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

Arrays.sort()

    public static <T> void sort(T[] a, int fromIndex, int toIndex,
                                Comparator<? super T> c) {
        if (c == null) {
            sort(a, fromIndex, toIndex);
        } else {
            rangeCheck(a.length, fromIndex, toIndex);
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, fromIndex, toIndex, c);
            else
                TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
        }
    }

1. 當Comparator == null時,調用sort(a, fromIndex, toIndex);如下

    public static void sort(Object[] a, int fromIndex, int toIndex) {
        rangeCheck(a.length, fromIndex, toIndex);
        //該分支將被刪除
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a, fromIndex, toIndex);
        else
            ComparableTimSort.sort(a, fromIndex, toIndex, null, 0, 0);
    }

    /** To be removed in a future release. */
    private static void legacyMergeSort(Object[] a,
                                        int fromIndex, int toIndex) {
        Object[] aux = copyOfRange(a, fromIndex, toIndex);
        mergeSort(aux, a, fromIndex, toIndex, -fromIndex);
    }

sort方法,參數[frimeIndex, toIndex)左閉右開,采用的算法能夠保證穩定性,相等元素按原來順序排列。傳入的數組部分有序則能保證事件復雜度遠小于nlg(n);若完全雜亂無序,則為n;若數組中存在連續升序或降序的情況都能被很好的利用起來

(這里的穩定是指比較相等的數據在排序之后仍然按照排序之前的前后順序排列。對于基本數據類型,穩定性沒有意義,而對于對象類型,穩定性是比較重要的,因為對象相等的判斷可能只是判斷關鍵屬性,最好保持相等對象的非關鍵屬性的順序與排序前一直;)

1) 當LegacyMergeSort.userRequested為true的情況下(該分支會在未來被棄用),采用legacyMergeSort,否則采用ComparableTimSort。

(為什么會被棄用,如一開始所說的。TimSort就應運而生,包括接下來介紹的ComparableTimSort,與前者基本相同唯一區別的是后者需要對象是Comparable可比較的,不需要特定Comparator,而前者利用提供的Comparator進行排序)

LegacyMergeSort.userRequested的字面意思大概就是“用戶請求傳統歸并排序”的意思,通過System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
設置
mergeSort()

//To be removed in a future release.未來會棄用
    @SuppressWarnings({"unchecked", "rawtypes"})
    private static void mergeSort(Object[] src,
                                  Object[] dest,
                                  int low,
                                  int high,
                                  int off) {
        int length = high - low;

        // Insertion sort on smallest arrays
        if (length < INSERTIONSORT_THRESHOLD) { //7
            for (int i=low; i<high; i++)
                for (int j=i; j>low &&
                         ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
                    swap(dest, j, j-1);
            return;
        }

        // Recursively sort halves of dest into src
        int destLow  = low;
        int destHigh = high;
        low  += off;
        high += off;
        int mid = (low + high) >>> 1;
        mergeSort(dest, src, low, mid, -off);
        mergeSort(dest, src, mid, high, -off);

        // If list is already sorted, just copy from src to dest.  This is an
        // optimization that results in faster sorts for nearly ordered lists.
        //這里說的是:mid兩側的元素都是有序的,若此時src[mid-1] <=src[mid],
          則不用在進行比較,節省時間
        if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
            System.arraycopy(src, low, dest, destLow, length);
            return;
        }

        // Merge sorted halves (now in src) into dest
        for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
            if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
                dest[i] = src[p++];
            else
                dest[i] = src[q++];
        }
    }

上面的代碼:當數組大小小于7時,采用插入排序,否則采用歸并排序

TimSort的重要思想是分區與合并
分區
分區的思想是掃描一次數組,把連續正序列(如果是升序排序,那么正序列就是升序序列)(也就是后面所指的run),如果是反序列,把分區里的元素反轉一下。 例如
1,2,3,6,4,5,8,6,4 劃分分區結果為
[1,2,3,6],[4,5,8],[6,4]
然后反轉反序列
[1,2,3,6],[4,5,8],[4,6]

合并
考慮一個極端的例子,比如分區的長度分別為 10000,10,1000,10,10,我們當然希望是先讓10個10合并成20, 20和1000合并成1020如此下去, 如果從從左往右順序合并的話,每次都用到10000這個數組和去小的數組合并,代價太大了。所以我們可以用一個策略來優化合并的順序。

2) 接著上面的例子,1)的LegacyMergeSort情況說完接下來調用的是ComparableTimSort.sort

    static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
        assert 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

        // If array is small, do a "mini-TimSort" with no merges
        //當數組大小小于32是,調用“mini-TimeSort”
        if (nRemaining < MIN_MERGE) { //32
            int initRunLen = countRunAndMakeAscending(a, lo, hi);
            binarySort(a, lo, hi, lo + initRunLen);
            return;
        }
..........未完待續
    }

首先來分析該函數中當數組大小小于32時,調用的“mini-TimeSort”情況:

  • 一開始調用了countRunAndMakeAscending(a, lo, hi)函數,得到一個長度initRunLen
    @SuppressWarnings({"unchecked", "rawtypes"})
    private static int countRunAndMakeAscending(Object[] a, int lo, int hi) {
        assert lo < 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) { // Descending
            while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)
                runHi++;
            reverseRange(a, lo, runHi);
        } else {                              // Ascending
            while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0)
                runHi++;
        }

        return runHi - lo;
    }

該函數有啥用?數組a,求從lo開始連續的升序或降序(會被反轉變成升序)的元素個數。求出的這個長度有啥用途?優化接下來調用的函數
binarySort(a, lo, lo + force, lo + runLen);代碼如下

//start參數傳進來的是lo+runLen,現在數組情況是a[lo, lo+runlen-1]為升序,
a[lo+runLen, hi)為亂序,該方法就是從lo+runLen開始往后一個個取出來與前面有序數組進行比較排序,
采用二分法

 @SuppressWarnings({"fallthrough", "rawtypes", "unchecked"})
    private static void binarySort(Object[] a, int lo, int hi, int start) {
        assert lo <= start && start <= hi;
        if (start == lo)
            start++;
        for ( ; start < hi; start++) {
            Comparable pivot = (Comparable) a[start];

            // Set left (and right) to the index where a[start] (pivot) belongs
            int left = lo;
            int right = start;
            assert left <= right;
            /*
             * Invariants:
             *   pivot >= all in [lo, left).
             *   pivot <  all in [right, start).
             */
            while (left < right) {
                int mid = (left + right) >>> 1;
                if (pivot.compareTo(a[mid]) < 0)
                    right = mid;
                else
                    left = mid + 1;
            }
            assert left == right;

            /*
             * The invariants still hold: pivot >= all in [lo, left) and
             * pivot < all in [left, start), so pivot belongs at left.  Note
             * that if there are elements equal to pivot, left points to the
             * first slot after them -- that's why this sort is stable.
             * Slide elements over to make room for pivot.
             */
            int n = start - left;  // The number of elements to move
            // Switch is just an optimization for arraycopy in default case
            switch (n) {
                case 2:  a[left + 2] = a[left + 1];
                case 1:  a[left + 1] = a[left];
                         break;
                default: System.arraycopy(a, left, a, left + 1, n);
            }
            a[left] = pivot;
        }
    }

前面提到的在數組大小<32情況下,采用“mini-TimeSort”,實質是二分排序,利用countRunAndMakeAscending求得的長度來進行優化。
int start參數傳進來的是lo+runLen,現在數組情況是a[lo, lo+runlen-1]為升序,a[lo+runLen, hi)為亂序,該方法就是從lo+runLen開始往后一個個取出來與前面有序數組進行比較排序,采用二分法。該函數時間復雜度為nlg(n),不過在最壞情況下需要n^2次移動。

現在來分析數組數目>32的情況:

接著上面未完的代碼
        /**
         * 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.
         */
        ComparableTimSort ts = new ComparableTimSort(a, work, workBase, workLen);
        int minRun = minRunLength(nRemaining);
        do {
            // 找出下個分區的起始位置,方法上面介紹過
            int runLen = countRunAndMakeAscending(a, lo, hi);

            // 如果run stack中的run太小, 就擴展至min(minRun, nRemaining)
            if (runLen < minRun) {
                int force = nRemaining <= minRun ? nRemaining : minRun;
                binarySort(a, lo, lo + force, lo + runLen);
                runLen = force;
            }

            // 把run放到run stack, 條件滿足會進行合并
            ts.pushRun(lo, runLen);
            ts.mergeCollapse();

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

        // Merge all remaining runs to complete sort
        assert lo == hi;
        //合并剩下的run
        ts.mergeForceCollapse();
        assert ts.stackSize == 1;

我們來分析代碼:首先ComparableTimSort ts = new ComparableTimSort(a, work, workBase, workLen);創建了ComparableTimSort對象

int stackLen = (len <    120  ?  5 :
                        len <   1542  ? 10 :
                        len < 119151  ? 24 : 49);
        runBase = new int[stackLen];
        runLen = new int[stackLen];

構造函數里對這三個變量賦值,他們是干嘛的?

    /**
     * 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;  // run的個數,run指的是分區
    private final int[] runBase; // runBase[0]第一個分區里第一個元素下標,runBase[1]第二個分區第一個元素下標.....
    private final int[] runLen; // runLen[0]第一個分區長度...

接著調用的是minRunLength,返回的數要么小于16,要么是16,要么介于[16, 32]之間

    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) { //32
            r |= (n & 1);
            n >>= 1;
        }
        return n + r;
    }

mergeCollapse:什么時候會進行合并呢?之所以進行判斷是為了防止這樣的情況:1000,10,100,10,10,這是五個分區的長度,最好的情況是先將小的分區合并,最后在和最大的分區合并,這個方法就是這個目的

//后兩個分區的和大于前一個分區,則中間的分區與最小的分區先合并
//否則合并后兩個分區
    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
            }
        }
    }

2. 當Comparable != null時,調用的方法類似

在Comparator != null情況下主要調用了TimSort.sort,看TimSort的代碼與ComparableTimSort幾乎一樣,只是在元素比較時用了調用者給的Comparator來進行比較。
暫時分析到這.........

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 四. 走向世界之巔——快速排序 你可能會以為歸并排序是最強的算法了,其實不然?;叵胍幌?,歸并的時間效率雖然高,但空...
    Leesper閱讀 1,771評論 9 7
  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,769評論 0 33
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 排序算法 冒泡排序 選擇排序 插入排序 快速排序(最常見) 希爾排序 歸并排序 源碼:Sorting 冒泡排序 冒...
    廖少少閱讀 2,743評論 12 101
  • 7 轉眼已到冬天,天空飄起來了雪花,似乎一切都朝著好的方向發展,作為大夫人的秀依忙著張羅新來的布匹,要給家里人做些...
    可人如霜閱讀 693評論 0 1