文档目录

一、核心流程:分治 + 合并

[3, 7, 8, 5, 2, 1, 9, 4]
         ↓ 分割
[3, 7, 8, 5]    [2, 1, 9, 4]
   ↓ 分割           ↓ 分割
[3, 7] [8, 5]   [2, 1] [9, 4]
   ↓ 分割           ↓ 分割
[3][7] [8][5]   [2][1] [9][4]
   ↓ 合并           ↓ 合并
[3, 7] [5, 8]   [1, 2] [4, 9]
   ↓ 合并           ↓ 合并
[3, 5, 7, 8]    [1, 2, 4, 9]
         ↓ 合并
[1, 2, 3, 4, 5, 7, 8, 9]

二、自顶向下(递归)实现

merge 整体操作流程:

  1. 创建临时数组:大小为 right - left + 1 用于存放合并结果。
  2. 双指针比较:i 指向左半区间 [left, mid],j 指向右半区间 [mid+1, right]。
  3. 取较小者:比较 nums[i] 和 nums[j],将较小的放入临时数组,对应指针前进。
  4. 收尾:将左半或右半剩余的元素全部放入临时数组。
  5. 写回:将临时数组的内容复制回原数组的 [left, right] 位置。

mergeSort 递归整体流程:

  1. 终止条件:left >= right 时返回。
  2. 分割:计算中点 mid,将数组分成两半。
  3. 递归左半:对 [left, mid] 排序。
  4. 递归右半:对 [mid+1, right] 排序。
  5. 合并:将两个有序子数组合并成一个有序数组。
void merge(vector<int>& nums, int left, int mid, int right) {
    vector<int> temp(right - left + 1);            // ① 临时数组
    int i = left, j = mid + 1, k = 0;              // ② i:左半起点, j:右半起点

    while (i <= mid && j <= right) {               // ③ 双指针取较小者
        if (nums[i] <= nums[j]) {
            temp[k++] = nums[i++];
        } else {
            temp[k++] = nums[j++];
        }
    }

    while (i <= mid) temp[k++] = nums[i++];        // ④ 左半剩余
    while (j <= right) temp[k++] = nums[j++];       // ⑤ 右半剩余

    for (int p = 0; p < temp.size(); p++) {        // ⑥ 写回原数组
        nums[left + p] = temp[p];
    }
}

void mergeSort(vector<int>& nums, int left, int right) {
    if (left >= right) return;                     // ① 递归终止

    int mid = left + (right - left) / 2;            // ② 取中点
    mergeSort(nums, left, mid);                     // ③ 递归排序左半
    mergeSort(nums, mid + 1, right);                // ④ 递归排序右半
    merge(nums, left, mid, right);                  // ⑤ 合并两个有序区间
}

时间复杂度:$O(N \log N)$(最好最坏都一样)。 空间复杂度:$O(N)$(合并时需要临时数组)+ $O(\log N)$(递归栈)。

三、自底向上(迭代)归并

整体操作流程:

  1. 步长控制:step 从 1 开始,每次翻倍(1→2→4→8…),代表当前合并的子数组大小。
  2. 遍历合并:每次以 step * 2 为步长遍历数组,合并相邻的两个长度为 step 的子数组。
  3. 计算边界:mid = left + step - 1 是左半的终点,right 是右半的终点(不超过数组末尾)。
  4. 调用 merge:合并 [left, mid] 和 [mid+1, right] 两个有序区间。
  5. 重复:直到 step ≥ n,整个数组有序。
void mergeSortIterative(vector<int>& nums) {
    int n = nums.size();

    for (int step = 1; step < n; step *= 2) {      // ① 步长 1,2,4,8...
        for (int left = 0; left < n - step; left += step * 2) {// ② 每两个子数组为一组
            int mid = left + step - 1;                // ③ 左半终点
            int right = min(left + step * 2 - 1, n - 1);// ④ 右半终点(防越界)
            merge(nums, left, mid, right);            // ⑤ 合并两个有序区间
        }
    }
}

自底向上 vs 自顶向下:

  • 自底向上:没有递归,没有 $O(\log N)$ 栈空间,只有 $O(N)$ 的临时数组。
  • 自顶向下:代码更直观,但递归深度 $\log N$。

四、外部排序——归并排序的核心应用

当数据量超过内存时,快排无法工作(需要随机访问整个数组),归并排序可以。

外部排序流程(假设内存只能存 100 个数,数据有 1000 个数):
1. 分 10 批,每批 100 个:
   读入 100 个 → 快排 → 写回磁盘为 "run1"
   读入 100 个 → 快排 → 写回磁盘为 "run2"
   ... 共 10 个有序文件

2. 多路归并:
   同时打开 10 个文件,各读第一个数
   选出最小的输出到结果文件,从对应文件读下一个
   类似多路归并排序的 merge 步骤

五、面试追问 Q&A

Q1:什么场景必须用归并排序而不是快排? A:

  1. 外部排序(数据量 > 内存):归并排序是唯一选择,快排需要随机访问整个数组。
  2. 需要稳定排序(相等元素的相对顺序不变):归并排序天然稳定,快排不稳定。
  3. 链表排序:归并排序可以在链表上 $O(1)$ 额外空间实现,快排需要随机访问。

Q2:std::stable_sort 的实现是什么? A:当元素数量足够分配临时内存时,用归并排序。当内存不足时,退化为 归并插入排序(TimSort 的变体)。