Axb的自我修養html
首頁java
目錄算法
[顯示]數組
前一陣遇到了一個使用Collections.sort()時報異常的問題,發現問題的緣由是JDK7的排序實現改成了TimSort,以後咱們又進一步研究了一下這個神奇的算法。服務器
先說一下爲何要研究這個異常,前幾天線上服務器發現日誌裏有偶發的異常:cors
1ide 2測試 3優化 4google 5 6 7 8 9 |
java.lang.IllegalArgumentException: Comparison method violates its general contract! at java.util.TimSort.mergeHi(TimSort.java:868) at java.util.TimSort.mergeAt(TimSort.java:485) at java.util.TimSort.mergeCollapse(TimSort.java:408) at java.util.TimSort.sort(TimSort.java:214) at java.util.TimSort.sort(TimSort.java:173) at java.util.Arrays.sort(Arrays.java:659) at java.util.Collections.sort(Collections.java:217) ... |
出錯部分的代碼以下:
1 2 3 4 5 6 7 |
List<Integer> list = getUserIds(); Collections.sort(list, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1>o2?1:-1; } }); |
google了一下:JDK7中的Collections.Sort方法實現中,若是兩個值是相等的,那麼compare方法須要返回0,不然可能會在排序時拋錯,而JDK6是沒有這個限制的。
這個問題在測試時並無出現,線上也只是小几率復現,如何穩定的復現這個問題?看了一下源代碼,拋出異常的那段源代碼讓人根本摸不着頭腦:
1 2 3 |
if (len2 == 0) { throw new IllegalArgumentException("Comparison method violates its general contract!"); } |
爲了解開這個困惑,咱們對java實現的Timsort代碼作了一些分析。
TimSort排序是一種優化的歸併排序,它將歸併排序(merge sort) 與插入排序(insertion sort) 結合,並進行了一些優化。對於已經部分排序的數組,時間複雜度遠低於 O(n log(n)),最好可達 O(n),對於隨機排序的數組,時間複雜度爲 O(nlog(n)),平均時間複雜度 O(nlog(n))。
它的總體思路是這樣的:
這篇文章就再也不過多的闡述Timsort總體思路了,有興趣能夠參考[譯]理解timsort, 第一部分:適應性歸併排序(Adaptive Mergesort)
重點說一下Timsort中的歸併。歸併過程相對普通的歸併排序作了必定的優化,假若有以下的一段數組:
首先把數組拆成兩個RunTask,這裏稱爲A段和B段,注意,A段和B段在物理地址上是連續的:
A段的起點爲base1,剩餘元素數量爲len1;B段起點爲base2,剩餘元素數量爲len2。取B點的起點值B[base2],在A段中進行二分查找,將A段中小於等於B[base2]的段做爲merge結果的起始部分;再取A段的終點值a[base1 + len1 – 1],在B段中二分查找,將B段中大於等於a[base1 + len1 – 1]值的段做爲結果的結束部分。
更形象的說,這裏把待歸併的數據「掐頭去尾」,只須要合併中間的數據就能夠了:
以後須要建立一個tmp數組,大小爲B段截取後的大小,並把B段剩餘的數據拷貝過去,由於合併過程當中這些數據會被覆蓋掉。
程序會記錄corsor1和corsor2,這是待歸併數據的指針,初始位置在A段和tmp段的末尾。同時會記錄合併後數組的dest指針,位置在原B段的末尾。
這裏還有一個小優化:生成dest指針時會直接把A段cursor1指向的數據拷貝到B段末尾,同時cursor–,dest–。由於以前(2)步的時候已經保證了arr[cursor1]>arr[dest]
進行歸併排序,這裏每次歸併比較時會記錄A和tmp段比較「勝利(大於對方)」的次數,比較失敗(小於對方)時會把勝利數清零。當有一個段的數據連續N次勝利時會激活另外一個優化策略,在這裏假設N爲4,下圖已是A段連續勝利了4次的狀況:
若是連續勝利N次,那麼能夠假設A段的數據平均大於B段,此時會用tmp[cursor2]的值在A[base0]至A[cursor1]中查找第一個小於tmp[cursor2]的索引k,並把A[k+1]到A[cursor1]的數據直接搬移到A[dest-len,dest]。
對於例子中的數據,tmp[cursor2]=8,在A數組中查找到小於8的第一個索引(-1),以後把A[0,1]填充到A[dest-1,dest],cursor1和dest指針左移兩個位置。
若是cursor1>=0,以後會再用curosr1指向的數據在tmp數組中查找,因爲這裏cursor1已是-1了,循環結束。
最後把tmp裏剩餘的數據拷貝到A數組的剩餘位置中,結束。
假設這裏實現的compare(obj o1,obj o2)以下:
1 2 3 |
public int compare(Integer o1, Integer o2) { return o1>o2?1:-1; } |
仍然是分紅A,B兩段:
在「掐頭去尾」的時候,這時會有一些變化,程序執行到compare(B[base2],A[base1])時返回-1,A的左側留下了兩個應該被切走的「5」。
接下來是正常的歸併過程。
這裏一樣會觸發「勝利」>N次邏輯
在A[base1,cursor1]中查找小於tmp[cursor2]的元素,複製,cursor1和dest左移兩位。
此時再用A[cursor1]在tmp中查找,tmp中全部的數據都被移入A數組,cursor二、dest左移4位。tmp2剩餘元素的數量(len2)爲0。
注意!
在第6步查找的時候,有A[base1+1]<tmp[0]
(tmp[0]的值等於沒有合併以前的B[base2])。
而第2步時,有B[base2]<A[base1]
而最初生成RunTask的時候,有A[base1]<=A[base1+1]
連起來就是B[base2]<A[base1]<=A[base1+1]<B[base2]
,這顯然是有問題的。
因此,當len2==0時,會拋出「Comparison method violates its general contract」異常。問題復現的條件是觸發「勝利N次」的優化,而且存在相似(A[base1]==A[base1+x])&&(A[base1+x]==B[base2])的數據排列。這裏應該還有幾種另外的觸發條件,精力有限,就再也不深究了。
TimSort in Java 7 OpenJDK 源代碼閱讀之 TimSort
1.JVM加入以下參數-Djava.util.Arrays.useLegacyMergeSort=true
,表示使用JDK6的排序算法
2.按照規定的比較規則進行值的返回,a==b 返回 0,ab 返回 1