一個開發人員寫了一段明顯有問題的排序代碼,大體以下:java
import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; public class Test { public static void main(String[] args) throws InterruptedException { //測試數據: List裏放Map,按Map裏的name字段排序 HashMap<String, String> a = new HashMap<String, String>(); a.put("name", "二"); HashMap<String, String> b = new HashMap<String, String>(); b.put("name", "一"); HashMap<String, String> c = new HashMap<String, String>(); c.put("name", "一"); HashMap<String, String> d = new HashMap<String, String>(); d.put("name", "四"); HashMap<String, String> e = new HashMap<String, String>(); e.put("name", "二"); HashMap<String, String> f = new HashMap<String, String>(); f.put("name", "三"); ArrayList<HashMap<String, String>> list = new ArrayList<>(); list.add(a); list.add(b); list.add(c); list.add(d); list.add(e); list.add(f); //排序:明顯有問題,由於只返回-1和0,也就是比較的時候永遠是小於等於 Collections.sort(list, new Comparator<HashMap<String, String>>() { @Override public int compare(HashMap<String, String> o1, HashMap<String, String> o2) { String n1 = o1.get("name"); String n2 = o2.get("name"); if (n1.equals("一")) { return -1; } if (n1.equals("二") && !n2.equals("一")) { return -1; } if (n1.equals("三") && !"一二".contains(n2)) { return -1; } if (n1.equals("四") && !"一二三".contains(n2)) { return -1; } return 0; } }); for(HashMap<String, String> x : list) { System.out.print(x.get("name")); } } }
按理這個排序是有問題的,可是無論怎麼改變測試數據,排序結果都是對的(測試數據量較小),上面代碼的輸出結果以下,用的jdk是1.7:算法
一一二二三四
可是,生產上是有問題的。數組
Collections.sort,最終調用了Arrays.sort,在1.7中,Arrays.sort作了修改。ide
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); } }
若是配置了java.util.Arrays.useLegacyMergeSort這個參數,那麼就走老的LegacyMergeSort,不然就走新的TimSort。函數
咱們在代碼里加上下面一句話,輸出結果就是亂序的,這符合預期。測試
System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
檢查了一下生產上JVM的參數,果真加了這個參數。code
可是爲何走TimSort的結果是對的呢?繼續分析TimSort的代碼,發現有一個特殊狀況的處理:排序
// If array is small, do a "mini-TimSort" with no merges if (nRemaining < MIN_MERGE) { //MIN_MERGE是32 int initRunLen = countRunAndMakeAscending(a, lo, hi, c); binarySort(a, lo, hi, lo + initRunLen, c); return; }
也就是在數組小於32的時候,進入這個裏面,而後沒有歸併。那咱們先來測試一下大於32的狀況。element
public class Test { public static void main(String[] args) throws InterruptedException { ArrayList<HashMap<String, String>> list = new ArrayList<>(); String[] xx = {"一","二","三","四"}; for(int i = 0; i < 35; i++) { HashMap<String,String> x = new HashMap<String,String>(); x.put("name", xx[(i+17)%4]); list.add(x); } Collections.sort(list, new Comparator<HashMap<String, String>>() { @Override public int compare(HashMap<String, String> o1, HashMap<String, String> o2) { String n1 = o1.get("name"); String n2 = o2.get("name"); if (n1.equals("一")) { return -1; } if (n1.equals("二") && !n2.equals("一")) { return -1; } if (n1.equals("三") && !"一二".contains(n2)) { return -1; } if (n1.equals("四") && !"一二三".contains(n2)) { return -1; } return 0; } }); for(HashMap<String, String> x : list) { System.out.print(x.get("name")); } } }
此次果真翻車了。開發
一一一一二二二二二三三三三三四四四四一一一一二二二二三三三三四四四四四
咱們經過代碼來看一下爲何小於32的時候排序成功了。
首先,咱們的比較函數,只有在真正小於或者等於狀況下返回了-1,其他狀況返回了0,包括大於的狀況也返回了0。
好比
兩個值 | 結果 |
---|---|
一一 | -1 |
一二 | -1 |
三二 | 0 |
四四 | -1 |
三一 | 0 |
爲了簡化,下面用阿拉伯數字代替
以211423爲例,
if (nRemaining < MIN_MERGE) { int initRunLen = countRunAndMakeAscending(a, lo, hi, c); binarySort(a, lo, hi, lo + initRunLen, c); return; }
第一步,是找到嚴格遞增或者遞減的最大長度,若是是升序,就不處理,降序的話,就reverse。
211423通過處理後變成了112 423,最大遞減長度爲3(由於1和1相比的結果爲-1,因此也被看成嚴格遞減),而後211被reverse成112
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; // Find end of run, and reverse range if descending if (c.compare(a[runHi++], a[lo]) < 0) { // Descending while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0) runHi++; reverseRange(a, lo, runHi); } else { // Ascending while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0) runHi++; } return runHi - lo; }
接下來,從第四個位置開始,找到它的位置,移動數據,讓每個數字找到合適的位置,具體的代碼以下:
private static <T> void binarySort(T[] a, int lo, int hi, int start, Comparator<? super T> c) { assert lo <= start && start <= hi; if (start == lo) start++; for ( ; start < hi; start++) { T pivot = 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 (c.compare(pivot, a[mid]) < 0) right = mid; else left = mid + 1; } assert left == right; 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; } }
對於112423的移動過程以下:
第一次:112 4 23, 在左邊找到合適4的位置,結果爲1124 23
第二次:1124 2 3, 在左邊找到2合適的位置,結果11224 3
第三次:11224 3,在左邊找到3合適的位置,結果爲112234,結束
在整個函數中,咱們發現了一個問題,那就是隻用到了c.compare(pivot, a[mid]) < 0,而大於0和等於0的狀況沒有用到,而咱們的比較函數正好是返回小於0的時候是正確的,因此並不會影響這個函數的執行結果。也就是說,只要真正小於的時候返回了-1,不小於的時候返回了0或者1,對這個函數是沒有影響的,正由於如此這個函數是個穩定排序。
可是在countRunAndMakeAscending這個函數裏用到了>=0。咱們看一下這種狀況,也就是數組的開頭是遞增的時候,會用到>=0
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; // Find end of run, and reverse range if descending if (c.compare(a[runHi++], a[lo]) < 0) { // Descending while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0) runHi++; reverseRange(a, lo, runHi); } else { // Ascending while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0) runHi++; } return runHi - lo; }
假設輸入的是1234123,前邊2和1相比結果是0,3和2也是0,4和3也是0,1和4是-1,因此最大遞增序列是1234,同時不用reverse,傳給下一個函數的輸入爲1234 123,結果三次插入,結果也是對的。
綜上分析能夠得出結論,就是由於在jdk 1.7中,若是數組小於32個元素,加入對於小於的比較都是-1, 其餘的都是0,那麼結果是正確的,這是由於算法自己的特性。可是大於32時,就不對了,會看到分段排好序了,這是由於歸併的時候比較結果都是0,致使沒有作歸併。
其實sort的Comparator是有坑的,必須把全部狀況都考慮周到,並且要知足如下特性:
1 ) 自反性: x , y 的比較結果和 y , x 的比較結果相反。
2 ) 傳遞性: x > y , y > z ,則 x > z 。
3 ) 對稱性: x = y ,則 x , z 比較結果和 y , z 比較結果相同。
上面的Comparator若是要寫的對,應該這麼寫,把全部狀況列出來,固然也能夠經過一些條件簡化,可是簡化的後果就是上面的結果,須要充分測試。
Collections.sort(list, new Comparator<HashMap<String, String>>() { @Override public int compare(HashMap<String, String> o1, HashMap<String, String> o2) { String n1 = o1.get("name"); String n2 = o2.get("name"); if (n1.equals("一") && n2.equals("一")) { return 0; } if (n1.equals("一") && n2.equals("二")) { return -1; } if (n1.equals("一") && n2.equals("三")) { return -1; } if (n1.equals("一") && n2.equals("四")) { return -1; } if (n1.equals("二") && n2.equals("一")) { return 1; } ...... } });