天天看點

源碼解析Collections.sort ——從一個逃過單測的 bug 說起

作者:京東雲開發者

本文從一個小明寫的bug 開始,講bug的發現、排查定位,并由此展開對涉及的算法進行圖解分析和源碼分析。

事情挺曲折的,因為小明的代碼是有單測的,讓小明更加笃定自己寫的沒問題。是以在排查的時候,也經曆了前世的500年,去排查排序後的list改動(主要是小明和同僚互相懷疑對方的代碼,不多說了)。

本文從問題定位之後開始講:

源碼解析Collections.sort ——從一個逃過單測的 bug 說起

前言

小明寫了一個自定義排序的代碼,簡化後如下。聰明的你快來幫小明review一下吧。

代碼

背景:有一批休息室,status是狀态,其中1表示空閑,8表示使用中,2表示在維修。需要按照 1 空閑< 8 使用中< 2 在維修的順序進行排序。

例如:輸入:[1,8, 2, 2, 8, 1, 8],期望輸出:[1, 1, 8, 8, 8, 2, 2]。list不為空,數量小于100。

環境:JDK 8

小明的代碼如下:

/**
* 排序
*/
private static int compare(Integer status1, Integer status2) {
        // 1<8<2 ,按照這樣的規則排序
        if (status2 != null && status1 != null) {
            // 2-維修中, 維修中排到最後面
            if (status2.equals(2)) {
                return -1;
            } else {
                // 8-使用中, 排在倒數第二,僅在維修中之前
                if (status2.equals(8) && !status1.equals(2)) {
                    return -1;
                }
            }
        }
        return 0;
    }

  //Test 
  public static void main(String[] args) {
       List<Integer> list = Lists.newArrayList(1, 8, 2, 2, 8, 1, 8);
        System.out.println("排序前:"+list);
        list.sort(Test::compare);
        System.out.println("排序後:"+list);
    }
           

看上面的代碼有問題麼?别急,咱們先給個入參試一下。

測試

[ 1, 8, 2, 2, 8, 1, 8 ]

public static void main(String[] args) {
       List<Integer> list = Lists.newArrayList(1, 8, 2, 2, 8, 1, 8);
        System.out.println("排序前:"+list);
        list.sort(Test::compare);
        System.out.println("排序後:"+list);
    }
           

輸出:

排序前:[1, 8, 2, 2, 8, 1, 8]
排序後:[1, 1, 8, 8, 8, 2, 2]
           

結論:結果是對的,符合預期 。 ( 按照 1 空閑< 8 使用中< 2 維修中的順序進行排序) 。

嗯,看起來排序是對的。但确實是有問題呢?

(小明OS :哪裡有問題?不可能有問題!我本地是好的!)

源碼解析Collections.sort ——從一個逃過單測的 bug 說起

那我們看看情景複現

情景複現

那有什麼問題呢?我們再給幾個入參試一下 。

case1 : 随機入參

[2, 8, 1, 2, 8, 8, 8, 2, 1]

輸出:

排序前:[2, 8, 1, 2, 8, 8, 8, 2, 1]
排序後:[1, 1, 8, 8, 8, 8, 2, 2, 2]
期望是:[1, 1, 8, 8, 8, 8, 2, 2, 2]
           

結論:結果對,符合預期 ✅。

case2 : 多增加一些數

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 2, 2]

輸出:

排序前:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 2, 2]
排序後:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 2, 2]
期望是:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1,  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 2, 2, 2, 2, 2, 2, 2]
           

結論:結果不對了,不符合預期 ❌。

case3 : 換幾個數

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 8, 2, 2, 2, 2, 2, 8, 8, 8, 8, 2, 2]

輸出:

排序前:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 8, 2, 2, 2, 2, 2, 8, 8, 8, 8, 2, 2]
排序後:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 8, 8, 8, 8, 8, 2, 2, 2, 2, 2, 2, 2]
期望是:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 8, 8, 8, 8, 8, 2, 2, 2, 2, 2, 2, 2]
           

結論:結果又對了??

源碼解析Collections.sort ——從一個逃過單測的 bug 說起

這是什麼情況?!

源碼解析Collections.sort ——從一個逃過單測的 bug 說起

小明有些慌了,越想越覺得奇怪,目前看起來有這樣幾個看起來些許離譜的結論:

1、可能是和資料量有關系(因為用32位以下的資料,多次Test 也沒發現問題),

2、一定和資料數值有關系。(32位以上,有的資料樣本沒問題,有的有問題)。

3、有問題都在中間部分,而兩邊是有序的,猜測像排序歸并導緻的問題。

定位

想查這個問題,小明有三個思路。

一是:代碼的邏輯比較,是有一些不完整的。那可以先試着改改代碼,通過這幾個失敗用例,然後在找深層原因。

二是:查查用的排序類,有沒有坑。用法有沒有特殊注意的,有沒有類似的案例。

三是:從源碼上,理清排序底層的邏輯,找到哪一個環節排序出了問題。

順着這三個思路,小明發現寫的代碼裡缺少傳回為1的場景。雖然小明不知道有沒有影響,但是試了試,發現好使。。但為啥呢?

private static int compare(Integer status1, Integer status2) {
        // 1<8<2 ,按照這樣的規則排序
        if (status2 != null && status1 != null) {
            // 2-維修中, 維修中排到最後面
            if (status2.equals(2)) {
                return -1;
            } else {
                // 8-使用中, 排在倒數第二,僅在維修中之前
                if (status2.equals(8) && !status1.equals(2)) {
                    return -1;
                }else{
                    return 1;
                }
            }
        }
        return 0;
    }
  }
           

然後小明看 Collections.sort 的坑,沒有看到和這個相關的。

接下來,還是要來調試代碼。最終定位是因為原來的compare 自定義代碼裡,對 compaer(2,1) 這種應該傳回1的情況 ,預設傳回了0。導緻在底層兩組資料歸并排序過程,誤以為1和2相等了。

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 2, 2]

見Debug部分:(具體分析内容在後邊源碼部分)

源碼解析Collections.sort ——從一個逃過單測的 bug 說起

解決方案

發現問題,就好解決了。方案是,要麼補充完善邏輯,要麼換用一種權重映射的排序方式。

1 優化代碼

/**
*  按照1<8<2排序
**/
private static int compare(Integer status1, Integer status2) {
        // 2大于8 大于1 ,按照這樣的規則排序
        if (status2 != null && status1 != null) {
            // 2-維修中, 維修中排到最後面
            if (status2.equals(2)) {
                return -1;
            } else {
                // 8-使用中, 排在倒數第二,僅在維修中之前
                if (status2.equals(8) && !status1.equals(2)) {
                    return -1;
                }else{
                    return 1;
                }
            }
        }
        return 0;
    }
           

2 改為權重等方式排序

當然,還有對于一些容易了解出錯的排序,也可以通過設定權重映射的方式進行排序。

小明忽然想起來,無論底層排序算法是什麼, 排序邏輯還是要完整。這一點也開發規約也是有的呀。

【注意】在JDK7版本及以上,為了讓Arrays.sort、Collections.sort正常工作,Comparator必須滿足以下三個條件,否則會抛出IllegalArgumentException異常。

1)比較x和y的結果應該與比較y和x的結果相反。

2)如果x>y,y>z,則x>z。

3)如果x=y,則比較x和z的結果應該與比較y和z的結果相同。

好了問題解決了,那我們接下來慢慢聊聊這裡Collections.sort 底層用的TimSort排序原理。以及為什麼32位及以上才有問題,為什麼正好是歸并過程有問題 ?

源碼解讀

JAVA 7 中集合類中的sort 開始,預設用TimSort排序方法 。Tim Sort,裡的Tim 也沒什麼特别的含義。Tim是這個算法的創始人Tim Peters 的名字。該算法首先在Python中應用,之後在 java 中應用。

TimSort :一種穩定的、自适應的、疊代的歸并排序,在部分排序數組上運作時需要的比較遠遠少于nlg (n)次,而在随機數組上運作時提供與傳統歸并排序相當的性能。像所有合适的歸并排序一樣,這種排序是穩定的,運作時間為O(n log n)(最壞情況)。在最壞的情況下,這種排序需要n/2個對象引用的臨時存儲空間;在最好的情況下,它隻需要少量的常量空間。這個實作改編自Tim Peters的Python清單排序

圖解 TimSort 排序原理

如果數組的長度小于32,直接采用二分法插入排序。(略)

如果數組的長度大于32,找到 單調上升段(或下降段,進行反轉),然後基于這個單調片段,通過插入排序的方式進行合并。如此反複歸并相鄰片段。

到這一步的時候,小明恍然大悟,怪不得32位數以下,沒有出現過問題呢。

這個算法裡有一個重要的概念,也可以了解為分段( 算法裡 run )。每個分段都是連續上升或下降的子串

源碼解析Collections.sort ——從一個逃過單測的 bug 說起

然後對下降的分段進行反轉,使其變為一個遞增的子串。這樣就可以得到若幹的分段,使得每個分段都單調遞增,後續就可以對這些分段進行合并。

源碼解析Collections.sort ——從一個逃過單測的 bug 說起

當然算法裡會計算出一個最小的分段長度(Java裡16-32之間),來控制分段的數量以保證效率。對那些不滿足最小長度的分區,會采用二分插入的方法,使其滿足最的長度。比如我們假設最小的長度是3,那此時由于第二段36 不符合最小長度3,會利用二分插入法,将8插入到第二段。即 368 就是第二段了。

分段劃分之後,下一步就是如何進行合并。

合并時,會将分區進行壓棧,并判斷是否需要和之前的分段做合并。當然還有一些更詳細的優化點,具體可看下文源碼部分。重點說一下,兩個分段如何進行合并。

假設以下内容:

第一個段包含元素:[1, 2, 3, 5, 6, 8, 9]

第二個段包含元素:[4, 6, 7, 8, 10, 11, 12]

第一個段在數組中出現在第二個段之前。請注意,實際段落長度不會這麼短。如前所述,段落長度應介于16到32之間。此處隻是提供示例以說明問題。

gallopRight():查找第二個段的第一個元素在第一個段中的位置。例如,在此示例中,位置為2。這意味着前兩個元素不需要參與合并,它們的位置不需要改變。

gallopLeft():查找第一個段的最後一個元素在第二個段中的位置。在此處,發現第二個段中的第四個元素為10。是以,第二個段中的10、11、12不需要參與合并,它們的位置也不需要改變。

源碼解析Collections.sort ——從一個逃過單測的 bug 說起

最終參與合并的段為:

第一段:[5,6,8,9]

第二段:[4, 6, 7, 8]

這樣參與合并的段的長度就大大減小了。 這裡就是我們上邊問題出現的地方。在gallopLeft 方法裡,

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2]

[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 2, 2]

查找第一個段的最後一個元素【2】在第二個段中的位置時,比較【2】和【1】時,得出了相等的結果。這有什麼影響呢?因為數組分段是單調遞增的,也就是說第一組裡最後一個(最大的)資料2,和第二組裡第一個(最小的)資料1 相等。那也就是說,第一個數組直接在第二個數組之前。即:

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 2, 2]

源碼解讀:Collections.sort 排序原理

入口

list.sort(Test::compare);

進入list sort

public void sort(Comparator<? super E> c) {
    final int expectedModCount = modCount;  // 目前 modCount 的值
    Arrays.sort((E[]) elementData, 0, size, c);  // 使用 Arrays.sort 對 elementData 數組進行排序
    if (modCount != expectedModCount) {  // 檢查排序過程中是否發生了并發修改
        throw new ConcurrentModificationException();
    }
    modCount++;  // 增加 modCount 的值,表示進行了一次修改
}
           

Arrays.sort()

進入Arrays.sort()方法

public static <T> void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c) {
    if (c == null) {
        sort(a, fromIndex, toIndex);  // 如果比較器為 null,調用預設的排序方法
    } else {
        rangeCheck(a.length, fromIndex, toIndex);  // 檢查 fromIndex 和 toIndex 的範圍是否合法
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a, fromIndex, toIndex, c);  // 如果指定使用傳統的歸并排序,則調用該方法
        else
            TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);  // 否則,調用 TimSort 進行排序
    }
}
           

TimSort.sort

我們重點看 TimSort.sort

/**
     * Sorts the given range, using the given workspace array slice
     * for temp storage when possible. This method is designed to be
     * invoked from public methods (in class Arrays) after performing
     * any necessary array bounds checks and expanding parameters into
     * the required forms.
     *
     * @param a the array to be sorted
     * @param lo the index of the first element, inclusive, to be sorted
     * @param hi the index of the last element, exclusive, to be sorted
     * @param c the comparator to use
     * @param work a workspace array (slice)
     * @param workBase origin of usable space in work array
     * @param workLen usable size of work array
     * @since 1.8
     */
    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;  // 數組長度為 0 或 1 時,無需排序

    // 如果數組長度較小,執行“迷你的 TimSort”而不進行合并操作
    if (nRemaining < MIN_MERGE) {
        int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
        binarySort(a, lo, hi, lo + initRunLen, c);
        return;
    }

    /**
     * 從左到右周遊數組一次,找到自然的 run,
     * 将短的自然 run 擴充到 minRun 的長度,并合并 run 以保持棧的不變性。
     */
    TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);
    int minRun = minRunLength(nRemaining);
    do {
        // 确定下一個 run
        int runLen = countRunAndMakeAscending(a, lo, hi, c);

        // 如果 run 很短,則擴充到 min(minRun, nRemaining) 的長度
        if (runLen < minRun) {
            int force = nRemaining <= minRun ? nRemaining : minRun;
            binarySort(a, lo, lo + force, lo + runLen, c);
            runLen = force;
        }

        // 将 run 推入待處理 run 棧,并可能進行合并
        ts.pushRun(lo, runLen);
        ts.mergeCollapse();

        // 前進以找到下一個 run
        lo += runLen;
        nRemaining -= runLen;
    } while (nRemaining != 0);

    // 合并所有剩餘的 run 以完成排序
    assert lo == hi;
    ts.mergeForceCollapse();
    assert ts.stackSize == 1;
}
           

countRunAndMakeAscending : 找到數組中的一段有序數字

countRunAndMakeAscending:方法的主要作用就是找到數組中的一段有序數字,并告訴我們它們的長度。如果這段數字是倒序的,它還會将它們反轉成正序。

private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi, Comparator<? super T> c) {
    assert lo < hi;
    int runHi = lo + 1;
    if (runHi == hi)
        return 1;

    // 找到 run 的結束位置,并在降序情況下反轉範圍
    if (c.compare(a[runHi++], a[lo]) < 0) {  // 降序
        while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
            runHi++;
        reverseRange(a, lo, runHi);
    } else {  // 升序
        while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
            runHi++;
    }

    return runHi - lo;
}
           

mergeCollapse : 将連續的有序小段合并成更大的有序段

mergeCollapse 的主要作用就是在排序過程中,将連續的有序小段合并成更大的有序段,以便更高效地進行排序。

/**
     * 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.
     * 每次将新的運作推入堆棧時,都會調用此方法,是以在進入方法時,對于 i < stackSize,滿足堆棧條件。
     */
 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--;
            // 在位置 n 處合并相鄰的運作
            mergeAt(n);
        } else if (runLen[n] <= runLen[n + 1]) {
            // 在位置 n 處合并相鄰的運作
            mergeAt(n);
        } else {
            // 堆棧條件已滿足,退出循環
            break;
        }
    }
}
           

mergeAt(n) : 把兩個有序的小段合并成一個更大的有序段

mergeAt(n) :它幫助我們把兩個有序的小段合并成一個更大的有序段,以便在排序過程中保持正确的順序。

/**
 * Merges the two runs at stack indices i and i+1. Run i must be
 * the penultimate or antepenultimate run on the stack. In other words,
 * i must be equal to stackSize-2 or stackSize-3.
 *
 * @param i stack index of the first of the two runs to merge
 */
private void mergeAt(int i) {
    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;

    // 記錄合并後的 run 長度;如果 i 是倒數第三個 run,也要滑動最後一個 run(不參與本次合并)
    runLen[i] = len1 + len2;
    if (i == stackSize - 3) {
        runBase[i + 1] = runBase[i + 2];
        runLen[i + 1] = runLen[i + 2];
    }
    stackSize--;

    // 找到 run2 中第一個元素在 run1 中的插入位置
    int k = gallopRight(a[base2], a, base1, len1, 0, c);
    assert k >= 0;
    base1 += k;
    len1 -= k;
    if (len1 == 0)
        return;

    // 找到 run1 中最後一個元素在 run2 中的插入位置
    len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
    assert len2 >= 0;
    if (len2 == 0)
        return;

    // 使用臨時數組(長度為 min(len1, len2))合并剩餘的 run
    if (len1 <= len2)
        mergeLo(base1, len1, base2, len2);
    else
        mergeHi(base1, len1, base2, len2);
}
           

gallopRigth && gallopLeft :在有序數組中快速查找目标元素的可能位置,便于合并

其中兩個主要的方法就是gallopRigth()和gallopLeft() 。這裡就是上面所說的 找元素的部分。

主要作用就是在有序數組中快速查找目标元素的可能位置,它采用一種跳躍式的查找政策,通過快速定位可能的位置,提高查找速度。

也就是上文中這一部分:

假設以下内容:

第一個段包含元素:[1,2,3,5,6,8,9]

第二個段包含元素:[4,6,7,8,10,11,12]

第一個段在數組中出現在第二個段之前。請注意,實際段落長度不會這麼短。如前所述,段落長度應介于16到32之間。此處隻是提供示例以說明問題。

gallopRight():查找第二個段的第一個元素在第一個段中的位置。例如,在此示例中,位置為2。這意味着前兩個元素不需要參與合并,它們的位置不需要改變。

gallopLeft():查找第一個段的最後一個元素在第二個段中的位置。在此處,發現第二個段中的第四個元素為10。是以,第二個段中的10、11、12不需要參與合并,它們的位置也不需要改變。

這樣參與合并的段的長度就大大減小,時間相應的就變短了(算法的優化點之一)。gallopLeft 代碼如下:

gallopLeft 方法用于在有序數組的指定範圍内進行快速查找,定位将指定鍵插入的位置或最左邊相等元素的索引。它使用跳躍式的查找政策,根據鍵與範圍内元素的比較結果,通過不斷調整步長進行左跳或右跳,直到找到合适的插入位置。最後,使用二分查找在找到的範圍内确定确切的插入位置,并傳回結果。這個方法的目标是提高查找效率。

/**
 * Locates the position at which to insert the specified key into the
 * specified sorted range; if the range contains an element equal to key,
 * returns the index of the leftmost equal element.
 *
 * @param key 要搜尋插入位置的鍵
 * @param a 要搜尋的數組
 * @param base 範圍内第一個元素的索引
 * @param len 範圍的長度;必須大于 0
 * @param hint 開始搜尋的索引,0 <= hint < n。hint 越接近結果,該方法的執行速度越快。
 * @param c 用于對範圍進行排序和搜尋的比較器
 * @return 整數 k,0 <= k <= n,滿足 a[b + k - 1] < key <= a[b + k],
 *    假設 a[b - 1] 是負無窮大,a[b + n] 是正無窮大。
 *    換句話說,鍵屬于索引 b + k 處;或者換句話說,
 *    數組 a 的前 k 個元素應該在鍵之前,後面的 n - k 個元素應該在鍵之後。
 */
private static <T> int gallopLeft(T key, T[] a, int base, int len, int hint,
                                  Comparator<? super T> c) {
    assert len > 0 && hint >= 0 && hint < len;
    int lastOfs = 0;
    int ofs = 1;
    if (c.compare(key, a[base + hint]) > 0) {
        // 向右跳躍,直到 a[base+hint+lastOfs] < key <= a[base+hint+ofs]
        int maxOfs = len - hint;
        while (ofs < maxOfs && c.compare(key, a[base + hint + ofs]) > 0) {
            lastOfs = ofs;
            ofs = (ofs << 1) + 1;
            if (ofs <= 0)   // 檢查 int 溢出
                ofs = maxOfs;
        }
        if (ofs > maxOfs)
            ofs = maxOfs;

        // 将偏移量相對于基準位置進行調整
        lastOfs += hint;
        ofs += hint;
    } else { // key <= a[base + hint]
        // 向左跳躍,直到 a[base+hint-ofs] < key <= a[base+hint-lastOfs]
        final int maxOfs = hint + 1;
        while (ofs < maxOfs && c.compare(key, a[base + hint - ofs]) <= 0) {
            lastOfs = ofs;
            ofs = (ofs << 1) + 1;
            if (ofs <= 0)   // 檢查 int 溢出
                ofs = maxOfs;
        }
        if (ofs > maxOfs)
            ofs = maxOfs;

        // 将偏移量相對于基準位置進行調整
        int tmp = lastOfs;
        lastOfs = hint - ofs;
        ofs = hint - tmp;
    }
    assert -1 <= lastOfs && lastOfs < ofs && ofs <= len;

    /*
     * 現在 a[base+lastOfs] < key <= a[base+ofs],
     * 是以鍵位于 lastOfs 的右側,但不超過 ofs 的位置。
     * 使用二分查找,在不變式 a[base + lastOfs - 1] < key <= a[base + ofs] 的條件下進行。
     */
    lastOfs++;
    while (lastOfs < ofs) {
        int m = lastOfs + ((ofs - lastOfs) >>> 1);

        if (c.compare(key, a[base + m]) > 0)
            lastOfs = m + 1;  // a[base + m] < key
        else
            ofs = m;          // key <= a[base + m]
    }
    assert lastOfs == ofs;    // 是以 a[base + ofs - 1] < key <= a[base + ofs]
    return ofs;
}
           

TimSort 算法的優缺點

優點

  1. 穩定性:TimSort 是一種穩定的排序算法,即相等元素的相對順序在排序後保持不變。
  2. 高效的處理小規模或部分有序數組:TimSort 在處理小規模數組時具有良好的性能,可以利用插入排序的優勢。此外,對于部分有序的數組,TimSort 也能快速識别并進行優化處理。
  3. 最壞情況下的時間複雜度是 O(n log n):在最壞情況下,TimSort 的時間複雜度與其他基于比較的排序算法(如快速排序和歸并排序)相同,都是 O(n log n)。
  4. 适用于大多數實際資料:TimSort 是一種自适應的排序算法,它能夠根據輸入資料的特性進行優化,适應不同的資料分布和大小。

缺點

  1. 需要額外的空間:TimSort 在合并階段需要額外的輔助空間,用于暫存部分數組。這可能導緻空間複雜度較高,特别是對于大規模資料排序時。
  2. 對于某些特殊情況效率較低:在處理某些特殊情況下,例如完全逆序的數組。

最後:

通過檢視 TimSort 的源碼,可以深入了解該算法的工作原理、核心步驟和關鍵邏輯。這有助于我們對排查問題時快速丁文問題,也有助于對算法的了解和知識的擴充。

另外 TimSort 是一種經過優化的排序算法,它采用了多種技巧來提高性能和效率。通過研究源碼,我們可以學習到一些優化技巧,例如插入、二分查找的優化、自适應調整等。這些技巧或許可以用在我們日後的開發場景中。當然,最重要的還是去逐漸體會、借鑒其實作方式和設計優化思想。

最後的最後,謝謝小明。

小明:

源碼解析Collections.sort ——從一個逃過單測的 bug 說起

參考:

排序算法——Timsort

java8中List中sort方法解析

作者:京東零售 馬丹妹

來源:京東雲開發者社群