快速排序是計算機科學中最經典的 原地排序 算法,JDK Arrays.sort()(基本類型)底層即採用雙軸快排。本文從分治思想、分區不變量、代碼實現、性能優化四個維度徹底講透 QuickSort。
選最後一個元素為基準,維護索引 i 指向「≤ 基準區間」尾部。遍歷 j,發現 arr[j] ≤ pivot 時 swap(++i, j)。最後 swap(i+1, end) 將基準歸位。
// Lomuto 分區:返回基準最終索引
private static int partitionLomuto(int[] a, int lo, int hi) {
int pivot = a[hi];
int i = lo - 1;
for (int j = lo; j < hi; j++) {
if (a[j] <= pivot) swap(a, ++i, j);
}
swap(a, i + 1, hi);
return i + 1;
}
缺點: 大量重複元素時退化為 O(n²);交換次數較多。
雙指針 i=lo-1, j=hi+1 向中間掃描,i 找 >pivot,j 找 j 作為分界點。
// Hoare 分區:返回分界索引 j(左區間 ≤ pivot,右區間 ≥ pivot)
private static int partitionHoare(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);
}
}
優勢: 交換次數少 ~30%,對重複元素友好,遞歸深度更淺。
public final class QuickSort {
private QuickSort() {}
/** 公開入口 */
public static void sort(int[] a) {
if (a == null || a.length < 2) return;
quickSort(a, 0, a.length - 1);
}
/** 核心遞歸:尾遞歸優化,遞歸深度 O(log n) */
private static void quickSort(int[] a, int lo, int hi) {
while (lo < hi) {
// 小區間改用插入排序(閾值經驗值 16~32)
if (hi - lo < 16) {
insertionSort(a, lo, hi);
return;
}
int p = partition(a, lo, hi);
// 先遞歸較短分支,尾遞歸消除較長分支棧幀
if (p - lo < hi - p) {
quickSort(a, lo, p);
lo = p + 1;
} else {
quickSort(a, p + 1, hi);
hi = p;
}
}
}
/** Hoare 分區 + 三數取中基準 */
private static int partition(int[] a, int lo, int hi) {
int mid = lo + (hi - lo) / 2;
// 三數取中:a[lo], a[mid], a[hi] 排序,中位數作基準
if (a[lo] > a[mid]) swap(a, lo, mid);
if (a[lo] > a[hi]) swap(a, lo, hi);
if (a[mid] > a[hi]) swap(a, mid, hi);
int pivot = a[mid];
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);
}
}
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;
}
// 測試
public static void main(String[] args) {
int[] data = {11, 213, 134, 44, 77, 78, 23, 43, 999, 0, -5, 44};
sort(data);
System.out.println(Arrays.toString(data));
}
}
| 指標 | 數值 | 備註 |
|---|---|---|
| 最佳時間 | O(n log n) | 每次分區均衡 |
| 平均時間 | O(n log n) | 隨機數據期望 |
| 最差時間 | O(n²) | 有序/逆序 + 固定基準 |
| 空間複雜度 | O(log n) | 遞歸棧深度(尾遞歸優化後) |
| 穩定性 | 不穩定 | 相等元素可能交換順序 |
n < 16 時改用插入排序,減少函數調用開銷。while 迴圈,保證棧深度 O(log n),防止 StackOverflowError。< pivot | = pivot | > pivot 三段,JDK Arrays.sort() 內部即採用此策略。Arrays.parallelSort(),基於 ForkJoinPool 多核加速。面試加分點: 能手寫 Hoare 分區、解釋為什麼返回
j而非i、說清尾遞歸優化原理、知道 JDK 實際用的是 Dual-Pivot Quicksort(雙軸快排)並簡述其分區邏輯。