🌗

分治与归并排序

大问题拆成小问题,各自排好,再合并——分而治之

💡 打个比方

班里 40 个同学要按身高排队,老师喊"都别动"很乱。换个办法:先两两一组,每 2 个人比一下站好;然后两个小组合并成 4 人小队,排好;再合并成 8 人、16 人、40 人大队。每一步都是"把两个已经排好的队伍,合成一个排好的队伍"。

这就是分治(divide & conquer):拆——把大问题拆成两个规模减半的小问题;治——小问题小到只剩 1 个元素(天然有序,基本情况);合——把两个排好的半区合并成一个。

合并是关键:两队都排好序了,每次只要比较两队队头,谁小谁先出列。这样合并一次只比 O(n) 次,而"拆"要拆 log n 层,所以总时间 O(n log n)——比冒泡的 O(n²) 快得多。

分治三件套:① 拆——分成规模减半的子问题;② 治——子问题递归解决,到基本情况(长度 1);③ 合——把子问题的解合并成本层的解。归并排序的"合"就是双队头比大小。

🖼️ 大图看明白

① 归并排序的递归树:拆 [8, 3, 7, 1] [8, 3, 7, 1] → [1, 3, 7, 8] [8, 3] → [3, 8] [7, 1] → [1, 7] [8] [3] [7] [1] 拆一半 拆一半 一直拆到每半只剩 1 个元素(天然有序)= 基本情况;再一层层合并回去。 ② 合并:两个已排好的半区,反复比较两队头 左半(已排好) 3 8 右半(已排好) 1 7 ① 3 对 1 → 1 小,1 出列 ② 3 对 7 → 3 小,3 出列 ③ 8 对 7 → 7 小,7 出列 ④ 右边空了 → 8 直接出列 合并结果:谁小谁先出列,写进新数组 1 3 7 8 一次合并 = O(n) 次比较;拆 log n 层 → 总共 O(n log n);需要一块长度 n 的辅助数组。 蓝色 = 来自左半,橙色 = 来自右半——结果两半掺着出列,所以稳定。

📏 怎么写归并排序:只要 3 步

第 1 步:拆——找出中点 mid,把区间 [l, r] 拆成左半 [l, mid] 和右半 [mid+1, r],分别递归排序。拆到区间只剩 1 个元素(l >= r)就停——基本情况:一个数天然有序。

第 2 步:合——两个半区都排好序了,开一个辅助数组,用两个指针 i、j 分别指着两队头,谁小谁先拷进辅助数组(相等时先拿左边,保证稳定);哪队先空了,就把另一队剩下的整体拷完。

第 3 步:抄回去——把辅助数组里的结果抄回原数组对应区间,这一层就排好了;一路返回,整个数组就排完了。

复杂度账本:每次合并 O(n);"拆"一共 log n 层;总时间 O(n log n);额外要一块长度 n 的辅助数组(空间 O(n));相等先取左 → 稳定排序。

💻 C++ 怎么写

int a[105], tmp[105];   // 原数组、辅助数组
// 合并两个已排好序的半区 [l,mid] 和 [mid+1,r]
void merge(int l, int mid, int r) {
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r) {
        if (a[i] <= a[j]) tmp[k++] = a[i++];  // 相等先取左,保证稳定
        else              tmp[k++] = a[j++];
    }
    while (i <= mid) tmp[k++] = a[i++];   // 左边剩下,整体拷
    while (j <= r)   tmp[k++] = a[j++];   // 右边剩下,整体拷
    for (int t = 0; t < k; t++) a[l + t] = tmp[t];  // 抄回原数组
}
// 分治:把 [l,r] 拆一半各自解决,再合并
void mergeSort(int l, int r) {
    if (l >= r) return;         // 基本情况:长度 1 天然有序
    int mid = (l + r) / 2;
    mergeSort(l, mid);          // 治:左半
    mergeSort(mid + 1, r);      // 治:右半
    merge(l, mid, r);           // 合
}
考点:基本情况是 l >= r(长度 1);merge 里两个 while 收尾不能漏(剩下的元素要整体拷走);总时间 O(n log n)、辅助空间 O(n)、稳定。

✋ 你来试试

题 1:用归并排序排 [5, 2, 8, 1],写出每一层合并的结果,最后数一数一共比较了几次?
拆:[5,2,8,1] → [5,2] 和 [8,1] → 四个单元素。合并:[5,2] → 5 对 2,取 2 再取 5,得 [2,5](比较 1 次);[8,1] 同理得 [1,8](比较 1 次)。最后合并 [2,5] 和 [1,8]:2 对 1 取 1 → 2 对 8 取 2 → 5 对 8 取 5 → 右边空了取 8,得 [1,2,5,8](比较 3 次)。一共 1+1+3 = 5 次比较。
题 2:为什么归并排序"永远"是 O(n log n),而冒泡、选择排序只有 O(n²)?它稳定吗?
因为归并排序的"拆"永远对半:不管输入长什么样,都会拆 log n 层,每层合并共 O(n) 次比较,所以时间稳定在 O(n log n),不会被坏数据坑。冒泡、选择排序是逐个比较交换,n 个数要约 n²/2 次,数据再友好也省不掉。稳定性:合并时相等元素先取左边,相同值的相对顺序不会变,所以归并排序是稳定排序。
🎯 一句话记住:归并排序 = 对半拆 + 双队头取小合并;基本情况是长度 1;O(n log n)、辅助空间 O(n)、稳定。