核心思想:分治
- 分解:將長度為 n 的數組遞歸二分,直到子數組長度為 1(天然有序)。
- 解決:長度 1 直接返回。
- 合併:將兩個有序子數組合併為一個有序數組(雙指針 O(n))。
合併兩個有序數組(核心子過程)
// 將 a[left..mid] 和 a[mid+1..right] 合併到 tmp,再拷回 a
private static void merge(int[] a, int left, int mid, int right, int[] tmp) {
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
tmp[k++] = (a[i] <= a[j]) ? a[i++] : a[j++]; // <= 保證穩定性
}
while (i <= mid) tmp[k++] = a[i++];
while (j <= right) tmp[k++] = a[j++];
System.arraycopy(tmp, 0, a, left, k); // 批量拷回,比循環快
}
遞歸版完整實現
public class MergeSortRecursive {
public static void sort(int[] a) {
if (a == null || a.length < 2) return;
int[] tmp = new int[a.length]; // 外部分配,避免遞歸層層 new
sort(a, 0, a.length - 1, tmp);
}
private static void sort(int[] a, int left, int right, int[] tmp) {
if (left >= right) return;
int mid = (left + right) >>> 1;
sort(a, left, mid, tmp);
sort(a, mid + 1, right, tmp);
// 優化:若已有序(a[mid] <= a[mid+1]),跳過合併
if (a[mid] > a[mid + 1]) merge(a, left, mid, right, tmp);
}
private static void merge(int[] a, int left, int mid, int right, int[] tmp) {
// 見上文
}
}
迭代版(自底向上,無遞歸棧溢出風險)
public class MergeSortIterative {
public static void sort(int[] a) {
if (a == null || a.length < 2) return;
int n = a.length;
int[] tmp = new int[n];
for (int width = 1; width < n; width <<= 1) { // 子數組寬度 1,2,4,8...
for (int left = 0; left < n; left += 2 * width) {
int mid = Math.min(left + width - 1, n - 1);
int right = Math.min(left + 2 * width - 1, n - 1);
if (mid < right && a[mid] > a[mid + 1]) { // 需合併
merge(a, left, mid, right, tmp);
}
}
}
}
// merge 同遞歸版
}
關鍵優化技巧
| 優化 | 代碼體現 | 效果 |
| 外部輔助數組 | 入口分配一次 tmp | 避免 O(n log n) 內存分配,減 GC |
| 已有序跳過合併 | if (a[mid] > a[mid+1]) merge() | 近乎有序數據接近 O(n) |
| 小數組插入排序 | 閾值 ~32 時調用 insertionSort | 減少遞歸開銷,JDK TimSort 同理 |
| System.arraycopy | 合併後批量拷回 | JVM 內部優化,比手寫循環快 5-10 倍 |
複雜度分析
- 時間:最好/平均/最壞均為 O(n log n)。分解高度 log n,每層合併總量 n。
- 空間:O(n) 輔助數組(非原地)。鏈表可 O(1) 空間。
- 穩定性:穩定(合併時
a[i] <= a[j] 先取左邊)。
與快速排序、堆排序對比
| 算法 | 平均時間 | 最壞時間 | 空間 | 穩定 | 適合場景 |
| 歸並排序 | O(n log n) | O(n log n) | O(n) | ✅ | 外部排序、鏈表、需穩定、近乎有序 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | ❌ | 內存數組通用、原地、常數因子小 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | ❌ | 嚴格 O(n log n)、嵌入式受限內存 |
| TimSort (JDK) | O(n log n) | O(n log n) | O(n) | ✅ | 實戰首選:Arrays.sort(Object[]) |
Java 實戰調用
int[] data = {5, 2, 9, 1, 5, 6};
MergeSortRecursive.sort(data); // 或 Iterative.sort(data)
System.out.println(Arrays.toString(data)); // [1, 2, 5, 5, 6, 9]
外部排序擴展(大文件排序)
- 將大文件分塊讀入內存,單獨歸並排序後寫入臨時文件。
- 對臨時文件進行 K 路歸並合併(優先隊列維護最小堆)。
- 最終輸出有序大文件。
總結
歸並排序以「穩定、最壞 O(n log n)、易並行化」著稱。掌握「外部輔助數組、已有序跳過、小數組插入排序」三大優化,即可寫出生產級代碼。日常開發直接用 Arrays.sort()/Collections.sort()(JDK 8+ 為 TimSort,融合歸並與插入優化),僅在外部排序、鏈表排序、教學驗證時手寫。
- 上一篇文章: Java 單例模式四大寫法深度對比:餓漢、靜態內部類、雙重檢查鎖、枚舉——含線程安全、序列化、反射攻擊防禦
- 下一篇文章: Java static 與 final 關鍵字深度對比:修飾變量、方法、類、代碼塊的語義差異與最佳實踐
Copyright ©
程式碼之家 All Rights Reserved