雖然生產環境首選 Arrays.sort()(TimSort,混合歸並與插入),但理解經典排序算法是面試必考、演算法思維訓練的基石。本文提供 10 種排序的標準實作、優化版本、效能測試框架與實測數據,助你建立完整知識體系。
| 算法 | 耗時 | 複雜度 | 穩定性 | 備註 |
|---|---|---|---|---|
| 快速排序 | 1 | O(n log n) | 不穩定 | 雙向掃描單樞紐,實測最快 |
| 希爾排序(優化2) | 1 | O(n1.3) | 不穩定 | 步長序列關鍵,接近 O(n log n) |
| 堆排序 | 2 | O(n log n) | 不穩定 | 原地排序,保證最壞 O(n log n) |
| 希爾排序(優化1) | 2 | - | - | 合併子序列掃描 |
| 希爾排序(基礎) | 2 | - | - | 分組插入排序 |
| 歸並排序 | 3 | O(n log n) | 穩定 | 需額外 O(n) 空間 |
| 二叉樹排序 | 3 | O(n log n)~O(n²) | 穩定 | 退化風險大,教學用 |
| 插入排序(優化1) | 127 | O(n²) | 穩定 | 近乎有序資料極快 |
| 插入排序(優化2) | 191 | - | - | 冒泡式前移 |
| 插入排序(基礎) | 218 | - | - | - |
| 選擇排序 | 252 | O(n²) | 不穩定 | 交換次數最少 |
| 冒泡排序(優化2) | 647 | O(n²) | 穩定 | 記錄最後交換位置 |
| 冒泡排序(優化1) | 662 | - | - | 旗標提前結束 |
| 冒泡排序(基礎) | 766 | - | - | 教學用,實務勿用 |
public static void quickSort(int[] a, int low, int high) {
if (low >= high) return;
int low0 = low, high0 = high;
boolean forward = false; // false: high 向左, true: low 向右
while (low0 != high0) {
if (a[low0] > a[high0]) {
swap(a, low0, high0);
forward = !forward; // 交換後改變掃描方向
}
if (forward) low0++; else high0--;
}
// 遞迴處理左右分區
quickSort(a, low, low0 - 1);
quickSort(a, high0 + 1, high);
}
關鍵洞見:forward 標誌確保樞紐元素在交換後自動移動到正確分區邊界,避免額外交換邏輯。
public static void shellSortAdvanced(int[] a) {
for (int gap = a.length / 2; gap > 0; gap /= 2) {
for (int j = gap; j < a.length; j++) {
if (a[j] < a[j - gap]) {
int tmp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > tmp) {
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = tmp;
}
}
}
}
步長選擇影響巨大:gap = gap/2 為最基礎序列,Sedgewick、Pratt、Ciura 序列可進一步提升 20-30% 效能。
public static int[] mergeSort(int[] a) {
if (a.length == 1) return a;
int mid = a.length / 2;
int[] left = mergeSort(Arrays.copyOfRange(a, 0, mid));
int[] right = mergeSort(Arrays.copyOfRange(a, mid, a.length));
return merge(left, right);
}
private static int[] merge(int[] a, int[] b) {
int[] res = new int[a.length + b.length];
int i = 0, j = 0, k = 0;
while (i < a.length && j < b.length)
res[k++] = (a[i] < b[j]) ? a[i++] : b[j++];
while (i < a.length) res[k++] = a[i++];
while (j < b.length) res[k++] = b[j++];
return res;
}
public static void heapSort(int[] a) {
// 建堆:從最後一個非葉節點向上調整
for (int i = (a.length - 2) / 2; i >= 0; i--)
maxHeapAdjust(a, i, a.length);
// 排序:逐個取出堆頂,縮小堆範圍
for (int i = a.length - 1; i > 0; i--) {
swap(a, 0, i);
maxHeapAdjust(a, 0, i);
}
}
private static void maxHeapAdjust(int[] a, int i, int len) {
int tmp = a[i];
for (int j = 2 * i + 1; j < len; j = 2 * j + 1) {
if (j + 1 < len && a[j + 1] > a[j]) j++; // 取較大子節點
if (a[j] < tmp) break; // 找到位置
a[i] = a[j]; // 子節點上移
i = j;
}
a[i] = tmp;
}
public static void performanceTest(int len) throws Exception {
int[] a = new int[len];
int times = 20;
String[] methods = {"bubbleSort", "quickSort", "mergeSort", ...};
for (String m : methods) {
Method method = SortUtil.class.getDeclaredMethod(m, int[].class);
long total = 0;
for (int t = 0; t < times; t++) {
randomize(a);
long start = System.nanoTime();
method.invoke(null, a);
total += System.nanoTime() - start;
}
System.out.printf("%s: %.2f ms%n", m, total / times / 1e6);
}
}
| 題目 | 核心答案 |
|---|---|
| 為何快排通常比歸並快? | 原地排序、緩存局部性好、遞迴深度較淺、常數因子小 |
| 何時用歸並排序? | 需穩定排序、外部排序、鏈結串列排序、保證 O(n log n) 最壞情況 |
| 希爾排序為何比插入快? | 大步長讓元素「長距離跳躍」,減少單步移動次數 |
| 堆排序如何建堆 O(n)? | 從最後非葉節點向上下沉,數學證明總比較次數 ≤ 2n |
| Java Arrays.sort 基本型別用什麼? | 雙軸快速排序,避免大量重複元素導致的退化 |
掌握排序不只是背誦代碼,而是理解:資料分布特性 → 選擇最適算法 → 针對性優化。實務中:
本文所有代碼已整理為可直接執行的 SortUtil 類,歡迎複製實驗、擴充測試。