本文從一個小明寫的bug 開始,講bug的發現、排查定位,并由此展開對涉及的算法進行圖解分析和源碼分析。
事情挺曲折的,因為小明的代碼是有單測的,讓小明更加笃定自己寫的沒問題。是以在排查的時候,也經曆了前世的500年,去排查排序後的list改動(主要是小明和同僚互相懷疑對方的代碼,不多說了)。
本文從問題定位之後開始講:
前言
小明寫了一個自定義排序的代碼,簡化後如下。聰明的你快來幫小明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 :哪裡有問題?不可能有問題!我本地是好的!)
那我們看看情景複現
情景複現
那有什麼問題呢?我們再給幾個入參試一下 。
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]
結論:結果又對了??
這是什麼情況?!
小明有些慌了,越想越覺得奇怪,目前看起來有這樣幾個看起來些許離譜的結論:
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部分:(具體分析内容在後邊源碼部分)
解決方案
發現問題,就好解決了。方案是,要麼補充完善邏輯,要麼換用一種權重映射的排序方式。
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 )。每個分段都是連續上升或下降的子串
然後對下降的分段進行反轉,使其變為一個遞增的子串。這樣就可以得到若幹的分段,使得每個分段都單調遞增,後續就可以對這些分段進行合并。
當然算法裡會計算出一個最小的分段長度(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不需要參與合并,它們的位置也不需要改變。
最終參與合并的段為:
第一段:[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 算法的優缺點
優點
- 穩定性:TimSort 是一種穩定的排序算法,即相等元素的相對順序在排序後保持不變。
- 高效的處理小規模或部分有序數組:TimSort 在處理小規模數組時具有良好的性能,可以利用插入排序的優勢。此外,對于部分有序的數組,TimSort 也能快速識别并進行優化處理。
- 最壞情況下的時間複雜度是 O(n log n):在最壞情況下,TimSort 的時間複雜度與其他基于比較的排序算法(如快速排序和歸并排序)相同,都是 O(n log n)。
- 适用于大多數實際資料:TimSort 是一種自适應的排序算法,它能夠根據輸入資料的特性進行優化,适應不同的資料分布和大小。
缺點
- 需要額外的空間:TimSort 在合并階段需要額外的輔助空間,用于暫存部分數組。這可能導緻空間複雜度較高,特别是對于大規模資料排序時。
- 對于某些特殊情況效率較低:在處理某些特殊情況下,例如完全逆序的數組。
最後:
通過檢視 TimSort 的源碼,可以深入了解該算法的工作原理、核心步驟和關鍵邏輯。這有助于我們對排查問題時快速丁文問題,也有助于對算法的了解和知識的擴充。
另外 TimSort 是一種經過優化的排序算法,它采用了多種技巧來提高性能和效率。通過研究源碼,我們可以學習到一些優化技巧,例如插入、二分查找的優化、自适應調整等。這些技巧或許可以用在我們日後的開發場景中。當然,最重要的還是去逐漸體會、借鑒其實作方式和設計優化思想。
最後的最後,謝謝小明。
小明:
參考:
排序算法——Timsort
java8中List中sort方法解析
作者:京東零售 馬丹妹
來源:京東雲開發者社群