ArrayList中有一個sort排序方法,只要你實現了Comparator的接口,按照你自己的排序業務進行實現,你只要告訴這個接口按照什麼類型進行排序就OK了。這種方式類似於設計模式中的策略模式,把流程劃分好,具體的業務邏輯由用戶指定。這時候我們需要帶着問題去看看裏面具體是如何實現的..
環境描述
JDK : 1.8
僞代碼:
public static void main(String[] args) {
// 初始化一組數據,這組數據可以是任意對象
int[] data = {7, 5, 1, 2, 6, 8, 10, 12, 4, 3, 9, 11, 13, 15, 16, 14};
// 構建成一個集合
List<Integer> list = new ArrayList<>();
for (int i = 0; i < data.length; i++) {
list.add(data[i]);
}
// 設定自己的比較方式
// 1順序 -1倒序
list.sort(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
int i = o1 > o2 ? 1 : -1;
System.out.println("開始比較 [o1] - " + o1 + "\t [o2] - " + o2);
return i;
}
});
// 打印結果
System.out.println(JSON.toJSONString(list));
}
問題
- 它是如何實現的 ?
實踐
跟蹤代碼
- ArrayList中的 sort方法
public void sort(Comparator<? super E> c) {
// 集合大小
final int expectedModCount = modCount;
// 將排序交給Arrays去實現
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
- Arrays中的sort方法又是交給一個TimSort類去實現的,我們直接看TimSort類的sort方法
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;
// 這裏表示如果大小小於2則沒有排序的必要了
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則會採用歸併排序
if (nRemaining < MIN_MERGE) {
// 這裏面會根據你數組中數據是遞增還是遞減的方式來劃分成兩塊
// 舉例 1,4,7,3,6,2 這裏一開始是遞增直到7結束,如果比較方法得出的的值是-1(遞減),
//則會將1,4,7先進行反轉得到 7,4,1 然後劃分成兩個邏輯部分{7,4,1} , {3,6,2} 進行歸併運算
// 這裏的返回值得到的就是劃分成兩部分的下標索引比如上面舉例的就是3
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
// 歸併排序
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
//////////////////////////////////// 這裏是數組長度大於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.
*/
TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);
// 得到一個最小的歸併長度,根據這個長度來判斷這組數據具體要歸併幾次
int minRun = minRunLength(nRemaining);
do {
// Identify next run
int runLen = countRunAndMakeAscending(a, lo, hi, c);
// If run is short, extend to min(minRun, nRemaining)
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen, c);
//運行長度
runLen = force;
}
// Push run onto pending-run stack, and maybe merge
// 將運行下標進行記錄
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;
ts.mergeForceCollapse();
assert ts.stackSize == 1;
}
private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,
Comparator<? super T> c) {
assert lo < hi;
// 得到下一個座標
int runHi = lo + 1;
// 如果都等於1
if (runHi == hi)
return 1;
// Find end of run, and reverse range if descending
//找到運行結束,如果下降,反向範圍
// 這裏面會根據你數組中數據是遞增還是遞減的方式來劃分成兩塊
// 舉例 1,4,7,3,6,2 這裏一開始是遞增直到7結束,如果比較方法得出的的值是-1(遞減),
//則會將1,4,7先進行反轉得到 7,4,1 然後劃分成兩個邏輯部分{7,4,1} , {3,6,2} 進行歸併運算
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++;
// 這裏的start 是等於上面劃分的臨界點的值,比如上面舉例的就是3,從3的下標值開始和一部分進行比較
for ( ; start < hi; start++) {
T pivot = a[start];
// Set left (and right) to the index where a[start] (pivot) belongs
// 左邊就相當於 {7,4,1}的下標,默認是0
int left = lo;
// 右邊就相當於{3,6,2}
int right = start;
assert left <= right;
/*
* Invariants:
* pivot >= all in [lo, left).
* pivot < all in [right, start).
*/
// 找歸併位置,必須左邊小於右邊
while (left < right) {
// 運算方式 , 你可以理解成 count / 2 得到的整數
int mid = (left + right) >>> 1;
// 如果小於0則改變right的位置
if (c.compare(pivot, a[mid]) < 0)
right = mid;
else
// 如果大於 則改變left值,
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.
*/
// 這裏是算出要移動的區間長度,如果區間長度在2以內,則交換一下位置就興了,如果大於2,則採用數據複製移動的方式
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;
}
}
畫圖理解一下:
-
初始化數組:
-
通過countRunAndMakeAscending方法,得到數據的走勢,是遞增還是遞減,邏輯上劃分成了兩個數組,劃分值是下標值3
-
循環B中的數據與A做歸併,這時候是從下標3開始,進行運算
運算流程:開始從第三個進行 , 這時候會定義三個變量 : 1. left 最左邊開始移動的數 2. right 最右邊開始移動數 3.start 開始移動數 初始化值: left = 0; right = 3; start = 3; 移動的運算公式 int mid = (left + right) >>> 1 ; // 這裏可以看作是 移動後的值/2 循環運算條件 : left < right 第一次運算: mid = 1; // 1的下標值是5. // 2 > 5 ? 決定是改變left還是right.. 這時候是改變right , right = mid; // 這時候right = 1; left = 0; //循環條件滿足 第二次運算: 1. 1>>>1 = 0 // 0的下標值是1 mid = 0; // 2 > 1 ? 這時候改變的是left left = mid + 1; // 這時候left = 1; 不會再做第三次運算了,因爲循環條件已經不滿足了,現在值是 1 > 1 了 這時候開始進行下一步,值移動 int n = start - left; // n = 3 - 1 =2 表示要移動2個值 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;
這時候我們看到的"大概"流程:
- 7 移動到2的位置
- 5移動到7的位置
-
5的位置由pivot代替,開始循環的變量值,不懂可以去看上面的代碼
然後開始下一個值的輪詢,一直到B組中的數據全部在A中找到對應的區間,完成排序.
總結
- ArrayList中的sort排序是採用歸併排序的。
[一個數組的值的趨勢(增長或者減少)打斷的地方開始劃分] - 當數組中的數據非常大的時候,會採用幾次歸併來完成排序.具體採用幾次歸併是minRunLength(nRemaining);方法去計算的.