快速排序是面試必考、工程常用的 O(n log n) 平均複雜度排序。本文從經典 Hoare 分區、隨機化基準防退化、三向切分處理重複鍵、尾遞歸消除、到 JDK Arrays.sort 雙軸快排 (Dual-Pivot Quicksort) 內核,層層遞進給出生產級實作與效能調優指南。
/** 原地快排,返回基準最終索引 */
private static int partition(int[] a, int lo, int hi) {
int pivot = a[lo + (hi - lo) / 2]; // 中位數作基準,減少有序陣列退化
int i = lo - 1, j = hi + 1;
while (true) {
do { i++; } while (a[i] < pivot);
do { j--; } while (a[j] > pivot);
if (i >= j) return j;
swap(a, i, j);
}
}
public static void quickSort(int[] a, int lo, int hi) {
if (lo < hi) {
int p = partition(a, lo, hi);
quickSort(a, lo, p);
quickSort(a, p + 1, hi);
}
}
| 優化 | 代碼片段 | 效果 |
|---|---|---|
| 隨機化基準 | swap(a, lo, random.nextInt(hi - lo + 1)); | 期望 O(n log n),消除惡意構造退化 |
| 小陣列切換插入排序 | if (hi - lo < 47) { insertionSort(a, lo, hi); return; } | 常數因子更小,JDK 閾值 47 |
| 尾遞歸消除 | while (lo < hi) { p = partition(...); quickSort(a, lo, p); lo = p + 1; } | 棧深度 O(log n) → O(1) |
| 三向切分 (Dijkstra) | 見下節 | 大量重複鍵 O(n) 而非 O(n log n) |
/** 將陣列分為 < pivot | = pivot | > pivot 三段 */
private static void quickSort3Way(int[] a, int lo, int hi) {
if (lo >= hi) return;
int lt = lo, gt = hi, i = lo + 1;
int pivot = a[lo]; // 已隨機交換到 lo
while (i <= gt) {
int cmp = Integer.compare(a[i], pivot);
if (cmp < 0) swap(a, lt++, i++);
else if (cmp > 0) swap(a, i, gt--);
else i++;
}
quickSort3Way(a, lo, lt - 1);
quickSort3Way(a, gt + 1, hi);
}
適用場景:用戶 ID 去重、日誌等級統計、基因序列分析等重複值極多的數據集。
pivot1 < pivot2,將陣列分為四段:< p1 | p1 ~ p2 | > p2
public final class QuickSort {
private static final int INSERTION_SORT_THRESHOLD = 47;
private static final Random RANDOM = new Random();
public static void sort(int[] a) {
if (a == null || a.length < 2) return;
sort(a, 0, a.length - 1);
}
private static void sort(int[] a, int lo, int hi) {
// 尾遞歸消除循環
while (hi - lo >= INSERTION_SORT_THRESHOLD) {
// 隨機化基準
swap(a, lo, lo + RANDOM.nextInt(hi - lo + 1));
int[] pivots = partition3Way(a, lo, hi); // 返回 [lt, gt]
// 先排較小分區,保證棧深度 O(log n)
if (pivots[0] - lo < hi - pivots[1]) {
sort(a, lo, pivots[0] - 1);
lo = pivots[1] + 1;
} else {
sort(a, pivots[1] + 1, hi);
hi = pivots[0] - 1;
}
}
insertionSort(a, lo, hi);
}
/** 三向切分,返回 {lt, gt} 使 a[lo..lt-1] < p < a[gt+1..hi] */
private static int[] partition3Way(int[] a, int lo, int hi) {
int pivot = a[lo];
int lt = lo, gt = hi, i = lo + 1;
while (i <= gt) {
int cmp = Integer.compare(a[i], pivot);
if (cmp < 0) swap(a, lt++, i++);
else if (cmp > 0) swap(a, i, gt--);
else i++;
}
return new int[]{lt, gt};
}
private static void insertionSort(int[] a, int lo, int hi) {
for (int i = lo + 1; i <= hi; i++) {
int key = a[i], j = i - 1;
while (j >= lo && a[j] > key) a[j + 1] = a[j--];
a[j + 1] = key;
}
}
private static void swap(int[] a, int i, int j) {
int t = a[i]; a[i] = a[j]; a[j] = t;
}
}
| 實作 | 耗時 (ms) | 相對 |
|---|---|---|
| Arrays.sort (Dual-Pivot) | ~1100 | 1.00x (基準) |
| 三向切分 + 尾遞歸消除 | ~1250 | 1.14x |
| 經典 Hoare + 隨機基準 | ~1450 | 1.32x |
| Collections.sort (TimSort) | ~1600 | 1.45x (物件陣列) |
a[lo] → 已排序陣列 O(n²)Arrays.sort/Collections.sort**,JDK 內核經過極致調優Comparator 而非繼承 Comparable,避免侵入領域模型快速排序的藝術在於「分區策略與遞歸控制的平衡」。理解 Hoare 分區、三向切分、雙軸快排的演進脈絡,能讓你在面試白板寫出無 Bug 代碼,也能在效能調優時知道為何 Arrays.sort 總是最快。記住:**除非極特殊場景,生產永遠用 JDK 內建排序。**