Java面試-List中的sort詳細解讀
最近看了一些排序相關的文章,因此比較好奇,Java中的排序是如何做的。本片文章介紹的是JDK1.8,List中的sort方法。
先來看看List中的sort是怎麼寫的:
@SuppressWarnings({"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
首先,你需要傳入一個比較器作為引數,這個好理解,畢竟你肯定要定一個比較標準。然後就是將list轉換成一個陣列,再對這個陣列進行排序,排序完之後,再利用iterator重新改變list。
接著,我們再來看看Arrays.sort:
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);
}
}
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
static final class LegacyMergeSort {
private static final boolean userRequested =
java.security.AccessController.doPrivileged(
new sun.security.action.GetBooleanAction(
"java.util.Arrays.useLegacyMergeSort")).booleanValue();
}
這樣可以看出,其實排序的核心就是TimSort,LegacyMergeSort大致意思是表明如果版本很舊的話,就用這個,新版本是不會採用這種排序方式的。
我們再來看看TimSort的實現:
private static final int MIN_MERGE = 32;
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; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
// 獲得最長的遞增序列
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
/**
* 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;
}
如果小於2個,代表不再不需要排序;如果小於32個,則採用最佳化的二分排序。怎麼最佳化的呢?首先獲得最長的遞增序列:
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).
*/
// start位置是遞增序列後的第一個數的位置
// 從前面的遞增序列中找出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;
/*
* 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.
*/
int n = start - left; // The number of elements to move
// Switch is just an optimization for arraycopy in default case
// 比pivot大的數往後移動一位
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;
}
}
好了,待排序數量小於32個的講完了,現在來說說大於等於32個情況。首先,獲得一個叫minRun
的東西,這是個啥含義呢:
int minRun = minRunLength(nRemaining);
private static int minRunLength(int n) {
assert n >= 0;
int r = 0; // Becomes 1 if any 1 bits are shifted off
while (n >= MIN_MERGE) {
// 這裡我沒搞懂的是為什麼不直接將(n & 1)賦值給r,而要做一次邏輯或。
r |= (n & 1);
n >>= 1;
}
return n + r;
}
各種位運算子,MIN_MERGE預設為32,如果n小於此值,那麼返回n本身。否則會將n不斷地右移,直到小於MIN_MERGE,同時記錄一個r值,r代表最後一次移位n時,n最低位是0還是1。
其實看註釋比較容易理解:
Returns the minimum acceptable run length for an array of the specified length. Natural runs shorter than this will be extended with binarySort.
Roughly speaking, the computation is: If n < MIN_MERGE, return n (it's too small to bother with fancy stuff).
Else if n is an exact power of 2, return MIN_MERGE/2.
Else return an int k, MIN_MERGE/2 <= k <= MIN_MERGE, such that n/k is close to, but strictly less than, an exact power of 2. For the rationale, see listsort.txt.
返回結果其實就是用於接下來的合併排序中。
接下來就是一個while迴圈
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
// lo——runLen為將要被歸併的範圍
ts.pushRun(lo, runLen);
// 歸併
ts.mergeCollapse();
// Advance to find next run
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
這樣,假設你的每次歸併排序的兩個序列為r1和r2,r1肯定是有序的,r2也已經被排成遞增序列了,因此這樣的歸併排序就比較特殊了。
為什麼要用歸併排序呢,因為歸併排序的時間複雜度永遠為O(nlogn),空間複雜度為O(n),以空間換取時間。
總結
好了,以上就是針對Java中的排序做的一次總結,但具體的歸併程式碼還沒有分析,其實我自己也沒有完全研究透,為什麼minRun
的取值是這樣的,這也和TimSort中的stackLen有關,有興趣的小夥伴可以在下方留言,我們可以一起探討。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3209/viewspace-2823991/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java 面試(二)| 詳細的MySql面試部分Java面試MySql
- java中Collections.sort排序詳解Java排序
- Java中的static詳細講解Java
- 詳細解讀Service Mesh的資料面Envoy
- BAT面試必問細節:關於Netty中的ByteBuf詳解BAT面試Netty
- java面試題總結-詳細分類Java面試題
- 不再怕面試被考字串---詳解Java中的字串面試字串Java
- 面試中超詳細的HTTP狀態碼面試HTTP
- SqueezeNet詳細解讀
- 史上最詳細的一線大廠Mysql面試題詳解MySql面試題
- PHP 詳細面試總結 (三 Redis 基礎詳解)PHP面試Redis
- python中list切片詳解Python
- Java核心內容面試題詳解Java面試題
- go 中 sort 如何排序,原始碼解讀Go排序原始碼
- 一看你就懂,超詳細 java 中的 ClassLoader 詳解Java
- 8年經驗面試官詳解 Java 面試祕訣面試Java
- list中add、set方法詳解
- linux命令詳解:sortLinux
- Linux sort 命令詳解Linux
- 詳細解讀go語言中的chnanelGoNaN
- Java類載入機制詳解【java面試題】Java面試題
- 手寫 Promise 詳細解讀Promise
- List面試題面試題
- [LeetCode] 148. Sort ListLeetCode
- LeetCode | 148. Sort ListLeetCode
- Kafka詳細教程加面試題Kafka面試題
- Java註解最全詳解(超級詳細)Java
- 阿里一道Java併發面試題 (詳細分析篇)阿里Java面試題
- Java面試題之Java類載入機制詳解!Java面試題
- 超詳細的Java面試題總結(三)之Java集合篇常見問題Java面試題
- java中cookie操作詳細JavaCookie
- 詳細解讀微服務的兩種模式微服務模式
- 從原始碼中學習Java集合中的List集合,詳細而透徹,一步到位原始碼Java
- Oracle SCN機制詳細解讀Oracle
- 矩陣分解--超詳細解讀矩陣
- Android BLE藍芽詳細解讀Android藍芽
- JavaScript中陣列Array.sort()排序方法詳解JavaScript陣列排序
- Java面試要注意哪些細節Java面試