Algorithm Design · Lecture 3-3

归并排序:把刀挪到正中间MergeSort

上一课 InsertionSort 把数组切成「n−1 + 1」:重的那块丢给递归神谕,轻的那块自己插。教授临走时留了句话:"It's a quadratic algorithm. We'll learn something that runs faster."(二次时间,不够快——我们会学到更快的。)更快的东西今天就来,而且它用的是同一套递归配方,只改一个动作:把刀从尾巴挪到正中间。切成两半,神谕要信两次,最后用一场「双指针缝合」收尾。快多少?答案藏在一个今天还解不开的式子里。

✓ 尺子:O·Ω·Θ 全套 ✓ 3-1 递归=归纳的硬币 ✓ 3-2 InsertionSort:切尾巴 ▶ 3-3 MergeSort:切中间 ○ 4-1 周四:解递推式
Ke Chen · May 26, 2026 承接 Lecture 3-2 可交互 中英双语
向下滚动开始 · scroll to begin
01

刀法之争:切尾巴,还是切中间 / Where to cut

教授开场先把两课缝在一起:"The idea is quite similar to insertion sort."(思路和插入排序非常像。)同一个递归骨架,唯一的分歧是——刀往哪里落。

先复盘 InsertionSort 的刀法:在 A[1..n] 的倒数第一格下刀,切出 A[1..n−1] 和孤零零的 A[n]。教授对这一刀的评语:"the first part is really heavy, it contains almost everything."(第一块特别重,几乎装下了一切。)重的那块整个丢给递归,轻的那块自己动手插进去——插一次最多要比 n−1 回,层层叠起来,worst case 结出 Θ(n²)。

MergeSort 的一句话宣言 "Instead of doing that, split the input array more evenly — in particular, we split it into two halves."(别切得那么偏——切匀一点,对半切。)两块一样重,谁也不吃亏;然后"throw each half to someone else":两个半份,请两位神谕各排一半,最后把两个有序的半成品缝合 (merge) 成一个整体。归并排序,名字就是这么来的。
Idea · MergeSort 三步(讲义原例)
1
对半切开 split in halves:[8,1,9,2][8,4,6,5]
2
各自排序 sort each half(两个递归调用):[1,2,8,9][4,5,6,8]
3
缝合两个有序半份 merge:[1,2,4,5,6,8,8,9]

输入是讲义全程使用的例子 [8, 1, 9, 2, 8, 4, 6, 5]——注意里面有两个 8,待会儿它俩会在缝合现场狭路相逢,逼我们表态「平局怎么办」。

切尾巴 · InsertionSort(3-2)
子问题:(n−1) + 1,一重一轻
递归调用:1 个
拼装动作:insert——把一个元素插进有序数组,最多 n−1 次比较
结局:worst case Θ(n²)
切中间 · MergeSort(本课)
子问题:⌈n/2⌉ + ⌊n/2⌋,平分秋色
递归调用:2 个
拼装动作:merge——把两个有序数组缝成一个,几次比较?(§04 数给你看)
结局:今天只写得出递推式
直觉先存疑 多请一位神谕,听起来更贵才对?教授自己也把问题挑明:"Merge sort is doing two recursive calls instead of one... it's curious which one is better."(两个递归调用对一个——好奇到底谁更快。)这笔账 §08 开列,§09 用实验先睹为快。
02

五行 MergeSort / The pseudocode

"We can translate that idea directly into the pseudocode."(想法可以直译成伪代码。)按 3-1 的配方逐行生长:兜底 → 变小 → 递归 → 拼装。

MergeSort(A[1..n])
if n ≤ 1 then return A // base case:空数组或单元素,天然有序
m ← ⌈n/2⌉ // 找中点,取上整 (ceiling)
BLMergeSort(A[1..m]) // 神谕一号:排前半
BRMergeSort(A[m+1..n]) // 神谕二号:排后半
return Merge(BL, BR) // 拼装:缝合两个有序半份
  • Base case:n ≤ 1。空数组有序,单元素数组也有序——"nothing to do, we just return the array itself."(啥也不用干,原样返回。)注意是 ≤ 1 而不是 == 0,这个「≤」一个字符扛着整个算法的终止性,§10 坑三见分晓。
  • 切点 m = ⌈n/2⌉(上整)。教授的口算示范:n = 5 时 m = 3,两半是 A[1..3](3 个)和 A[4..5](2 个)——"the first half is slightly larger, just one larger."(前半最多只比后半大 1。)n 是偶数时正好对半。
  • 两个递归调用 = 借两次黑盒。3-1 的神谕心态原封不动:不追调用栈,信任 BL、BR 各自交回一份排好序的半成品。这份信任待会儿由归纳法背书(§07)。
  • 最后一行拼装。两个有序半份进,一个有序整体出——全部新的技术含量都压在这个 Merge 上。
铁律体检(3-1 的例行公事,每写一个新递归都要做) ① base case 有吗?——有,n ≤ 1 兜底。② 每条递归路径都严格变小吗?——n ≥ 2 时,前半 size ⌈n/2⌉ ≤ n−1,后半 size ⌊n/2⌋ ≤ n−1,且两者都 ≥ 1:都严格变小,都是合法输入 ✓。(自己代 n=2、n=3 各验一遍:2 → 1+1;3 → 2+1。)
但这还不是一个算法! 教授:"This is not a complete algorithm, because I haven't told you how do we implement the merge function."(还不完整——我没说 Merge 怎么实现。)L1-1 的三支柱之一是 unambiguous:每一步都必须说清。最后一行还欠着一份说明书,下一节补齐。
03

Merge:一次比较,换一步变小 / Merge, recursively

既然全课都在递归主题里,Merge 当然也递归着写。教授还故意把它写得更慷慨:"k and ℓ can be arbitrary numbers... this merge procedure is useful by itself."(两个输入的长度随意,不必相等——这个过程本身就值得单独拥有。)

Problem · Merge(讲义原文)
Input:数组 X[1..k] 与 Y[1..ℓ],各自已排好序(升序)——这是前置条件 (precondition)
Output:含全部 k+ℓ 个元素的一个升序数组

MergeSort 里喂给它的两半 size 相等或只差 1,但 Merge 不挑食,长短随意——这份慷慨在挑战题 C4 会派上大用场。伪代码:

Merge(X[1..k], Y[1..ℓ])
if X is empty then return Y // base case ①
if Y is empty then return X // base case ②
if X[1] ≤ Y[1] then
return [X[1], Merge(X[2..k], Y)] // X 头出列,X 缩 1
else
return [Y[1], Merge(X, Y[2..ℓ])] // Y 头出列,Y 缩 1

为什么 base case 有两个?

输入有两个数组,兜底就要把两边都兜住:X 空了就把 Y 整个交出去,Y 空了就交 X——"you're trying to merge two sorted arrays, if one of them is empty, well, you just return the other one. There's nothing to merge."(一边空了,没什么可缝的,另一边本身就是答案。)注意这个 base case 不是摆设:它一口气交付一整段数组,待会儿在合并机里你会看到它「白送」若干元素。

递归情形:最小的工作,换最小的变小

教授把设计问题问到了骨头上:"What's the minimum amount of work we can do to make the input smaller?"(把输入变小,最少要干多少活?)答案:一次比较。比较两个数组的头一个元素 X[1] 和 Y[1]——赢家(较小者)有资格直接站上输出的第一位,理由是一个干净的三段论:

三段论 · 凭一次比较就敢定「全场最小」 ① X 已排序 ⇒ X[1] 是 X 全体的最小;② Y 已排序 ⇒ Y[1] 是 Y 全体的最小;③ 若 X[1] ≤ Y[1] ⇒ X[1] 是全部 k+ℓ 个元素的最小。升序输出要求最小者先行,所以 X[1] 放第一位,天经地义。——注意 ①② 全靠前置条件撑着:输入不有序,这个三段论当场塌方(§10 坑二)。

然后是「变小」:贡献了头元素的那个数组去掉头,X[1..k] 缩成 X[2..k],size 减 1;Y 原封不动。教授连安慰都替你想好了:"Smaller by 1 is smaller, and smaller is good."(小 1 也是小,小就够了。)于是递归合法,神谕接管剩下的一切。X[1] > Y[1] 时对称地走 else 支。

数清楚:写了两个,只执行一个 伪代码里出现了两处 Merge(...),但它们躺在 if-else 的两支里——每层恰好执行一个递归调用。教授特意放慢语速:"notice here is the if-else structure... we're only doing one recursive call."这一个细节马上决定 S(n) 的形状(§05),也是小测和考试的常客:数递归调用,数的是「执行了几个」,不是「写了几个」。
Merge 的正确性:归纳法(讲义 2/4 页的四条 bullet,展开成完整写法) · Write-up
命题 P(n):对任意两个各自有序、总长 k+ℓ = n 的数组 X、Y,Merge(X, Y) 返回含全部 n 个元素的升序数组。(两个输入合并计尺寸,归纳变量是总长 n。)
Base case ×2 X 空:返回 Y——Y 本身有序且恰含全部元素 ✓。Y 空:对称 ✓。(n = 0 或任一边为空的情形全部落进这两行。)
Inductive hypothesis 假设 P(m) 对一切 m < n 成立:总长更短的有序数组对,Merge 都能正确缝合。
Inductive step · 情形 X[1] ≤ Y[1] 由三段论,X[1] 是全场最小 → 站第一位正确。递归调用 Merge(X[2..k], Y):有序数组去掉头仍然有序(前置条件保住了),总长 n−1 < n → 被 IH 覆盖 → 返回其余 n−1 个元素的正确升序合并。X[1] 不超过其中任何元素(它是全场最小),前接之后整串仍升序。∎ 这正是讲义那句 "After prepending X[1], the result remains sorted."
Inductive step · 情形 X[1] > Y[1] "Similar for X[1] > Y[1]."——把 X、Y 角色互换,逐字重放。∎
品一品(3-1 的硬币又转回来了) 两个 base case ↔ 归纳证明的 base case;那一个递归调用 ↔ 归纳假设;「比较 + 去头」↔ 归纳步里唯一要亲手检查的部分。教授:"it's essentially the induction proof... very similar to the algorithm itself."算法怎么写,证明就怎么长。
04

实验室:双指针合并机 / Lab: the merge machine

教授明明叮嘱过「别开黑盒」,这次却自己没忍住:"it's really tempting to look inside the black box and see how it's happening. I'm fulfilling that request... let's do this anyways, kind of fun."(忍不住想看?我来满足你,反正挺好玩。)这是全课程第二次官方开盒——盒里是两根红箭头。

互动 · 双指针合并机:亲手缝合两个有序数组
场景 A:讲义原例 场景 B:完全交错(最费) 场景 C:一边倒(最省)
BL = X
BR = Y
Output
比较次数
0
已输出 / 总数
0 / 8

图例:▲ = 红箭头(当前被比较的头元素);输出格字色 蓝 = 来自上排 X,紫 = 来自下排 Y;虚线格 = 已出列。

场景 A 里有一场重头戏:两个 8 狭路相逢。教授的现场裁决:"eight versus eight — it's a draw, let's say that the top one wins, breaking the tie arbitrarily."(平局!就让上面的赢吧——怎么破平局无所谓。)伪代码里这份约定写作 X[1] ≤ Y[1] 的那个「≤」:相等时走 if 支,X 先出列。破法随意,不影响正确性——反正两个 8 谁先谁后,数组照样有序。

同一场缝合还有第三种视角——把递归调用链直接写在纸上(每行就是一层递归,和你刚才按的每一步一一对应):

// 场景 A 的递归展开链:7 次比较,一次 base case
Merge([1,2,8,9], [4,5,6,8])
= [1, Merge([2,8,9], [4,5,6,8])] // 1 ≤ 4
= [1, 2, Merge([8,9], [4,5,6,8])] // 2 ≤ 4
= [1, 2, 4, Merge([8,9], [5,6,8])] // 8 > 4
= [1, 2, 4, 5, Merge([8,9], [6,8])] // 8 > 5
= [1, 2, 4, 5, 6, Merge([8,9], [8])] // 8 > 6
= [1, 2, 4, 5, 6, 8, Merge([9], [8])] // 8 ≤ 8 平局,上面赢
= [1, 2, 4, 5, 6, 8, 8, Merge([9], [ ])] // 9 > 8
= [1, 2, 4, 5, 6, 8, 8, 9] // base case ②:Y 空,白送 9 ∎
开盒结论(教授等的就是这句) "I hope you agree with me, we didn't really learn anything new from this whole procedure. All we really do is compare the first elements of the two arrays, then recursive call... So I really encourage you to not think in this way. Just trust your inductive hypothesis, trust your recursive calls."(什么新东西也没看到——每一层干的都是同一件事。所以别这么想问题:信任归纳假设,信任递归调用。)看一次,过瘾;看懂之后,永远不用再看——全部洞见早就写在那六行伪代码里。

三个场景的比较次数:7、7、3。上限是 n−1(证明在挑战题 C2 等你),下限能低到 min(k, ℓ)——两边谁先耗尽,决定多少元素是 base case 白送的。

05

S(n):似曾相识的递推式 / Time of Merge

三问的第二问,先问 Merge:多快?但这次有个新情况——输入是两个数组,时间函数的自变量 n 该是什么?

教授的处理干脆利落:X 和 Y "are not really used differently... they're both just input arrays containing numbers"(两个数组没有被区别对待,都只是装数字的容器),所以合并记账——令 n = k + ℓ,总 size。Merge 的时间复杂度函数记作 S(n)。然后照 L1-2 的老规矩逐行数:

  • 两行 base case + 一次 X[1] ≤ Y[1] 的比较:常数工作,记 O(1);
  • if-else 只走一支 → 恰好一个递归调用,其输入总 size = (k−1) + ℓ 或 k + (ℓ−1) = n − 1(贡献方缩 1,另一方不动);
  • 没有别的了。
S(n) = S(n−1) + O(1)
似曾相识燕归来 教授:"This is exactly the same recurrence relation as we saw for the insert function in insertion sort."(和 3-2 里 insert 函数的递推式一模一样。)两个长相完全不同的过程——一个往有序数组里插元素,一个缝合两个有序数组——成本结构却孪生:每层常数工作,每层总规模减 1。递推式是比伪代码更深一层的「指纹」。
诚实声明 · 本课未解 "We didn't really have a solution for that something yet. But at least we have a recurrence relation... we'll talk about it more on Thursday."(还没解出 S(n) = O(什么),但递推式已经到手——周四细讲。)手感可以先有:合并机数出比较 ≤ n−1 次,每次比较附带常数杂务,「线性感」呼之欲出;3-1 剥过的洋葱 T(n) = T(n−1) + c 也长这样,当时剥出了 O(n)。但手感 ≠ 定理,闭式解等周四的正式工具。
06

实验室:整棵递归树 / Lab: the full run

教授只开了 Merge 一个盒。我们得寸进尺一次:把整趟 MergeSort([8,1,9,2,8,4,6,5]) 全程摊开——先一路切到底,再一路缝回来。(讲义没有这张图,属自费参观;看完记得把盒子还回去。)

互动 · 整棵递归树:切下去,缝回来
本层比较
累计比较
0
参观纪要(三条,都值得抄进笔记)「切」的路上一次比较都没发生——分家不花钱,全部 17 次比较都花在「缝」的归途上;② 每一层缝合的总比较数是 4、6、7,每层都不超过 n−1 = 7;③ 从 size 8 切到 size 1 只用了 3 刀,而 log₂8 = 3。「每层至多 n−1 次」×「一共 log₂n 层」——这两个数字握手的地方,就是 T(n) 的谜底埋藏的位置。(本课未详述,教授把它整个留给了周四;你现在闻到的只是味道,还不是菜。)

似曾相识?3-1 挑战题 C3 的 HalfSum 就是这个形状(对半切、双递归、拼装),当时题解剧透:「这种形状,几周后有个大名鼎鼎的亲戚——MergeSort。」亲戚今天登门了。另外注意:MergeSort 的两个递归调用都会执行(树每层真的裂开),对比 Merge 的 if-else 只执行一个——§08 的式子里会各留一个影子。

07

MergeSort 的正确性:考卷发到你手上 / Correctness (Exercise)

讲义 4/4 页在「Correctness?」后面只写了五个词:"Proof by induction. (Exercise)"。教授的原话把台阶都搭好了:"three steps — base case, inductive hypothesis, use that hypothesis to complete your inductive step... I'm sure you can fill in this complete proof."

动笔前清点装备,三件全是旧的:① 归纳三步模板(3-1);② Merge 的正确性定理(§03 已证,归纳步里可以直接引用——教授:"inside your inductive step, obviously you're going to need the correctness of the merge procedure, which we just proved");③ 强归纳的手感(3-1 挑战题 C3:当子问题是对半切出来的,「对 n−1 正确」的弱假设接不住,必须假设「对一切 < n 正确」)。先自己写一遍——这同时就是挑战题 C1——再展开对答案:

完整证明:MergeSort 的正确性(把 Exercise 做掉) · Write-up
命题 P(n):对任意 size n 的输入数组 A,MergeSort(A[1..n]) 返回「A 的全部元素的升序重排」。
Base case n ≤ 1:空数组与单元素数组本身就是自己的升序重排,算法第一行原样返回。✓(不依赖任何假设,独立验证完毕。)
Inductive hypothesis(强形式) 假设 P(m) 对一切 m < n 成立:更小的数组,MergeSort 都能正确排序。
Inductive step 设 n ≥ 2,m = ⌈n/2⌉。两个递归调用的输入 size 分别是 m 和 n−m = ⌊n/2⌋,由 §02 的铁律体检:都 ≥ 1 且 ≤ n−1 < n → 都落在 IH 的覆盖范围内 → BL 是 A[1..m] 的升序重排,BR 是 A[m+1..n] 的升序重排。两半合起来恰好是 A 的全部元素(中点一刀,不重不漏)。
引用 Merge 定理 BL、BR 各自有序——Merge 的前置条件满足;由 §03 已证的定理,Merge(BL, BR) 返回这 n 个元素的升序合并。这正是 A 的升序重排。∎
品一品 ① 归纳步里亲手检查的只有两件事:子问题严格变小(体检早做过了)、拼装步骤正确(引用一条已证定理)。其余全部记在归纳假设的账上——"correctness follows from the inductive hypothesis",递归证明的万能收尾又一次上岗。
品一品 ② · 为什么必须强归纳 n = 8 时子问题 size 是 4 和 4——不是 7。假设只写「P(n−1) 成立」,这两个调用就没有任何保证,归纳步当场断链。子问题一步跳多远是算法说了算,所以模板一律写「for all sizes < n」。(3-1 C3 的教训,今天成了正课。)
结构复盘 · 证明的依赖链 先证内层工具(Merge),再证外层算法(MergeSort),外层的归纳步引用内层的定理——和 3-2 一模一样:先证 insert,再证 InsertionSort。递归算法的正确性证明是搭积木,不是一口气吞大象。
08

T(n):一行式子,两个未知 / The recurrence cliffhanger

第二问轮到 MergeSort 自己:多快?老规矩,设 T(n) 为 MergeSort 处理 size n 输入的时间,照伪代码逐行记账——然后式子停在了一个红色的问号上。

  • 第 1 行 base case、第 2 行整数除法:常数,O(1);
  • 第 3 行:递归调用,输入 size ⌈n/2⌉ → 按定义记 T(⌈n/2⌉);
  • 第 4 行:递归调用,输入 size n − ⌈n/2⌉ = ⌊n/2⌋ → 记 T(⌊n/2⌋);两个调用都会执行(没有 if-else 帮你二选一——§06 的树亲眼看过,每层真的裂开);
  • 第 5 行:调用 Merge,总输入 size n → 记 S(n)(§05 的函数,原样嵌进来)。
T(n) = T(⌈n/2⌉) + T(n−⌈n/2⌉) + S(n)
     = T(⌈n/2⌉) + T(⌊n/2⌋) + S(n)
T(n) = ? // 讲义原文如此:红色问号,悬而未解

对比一下历代记账,新东西一目了然:

  • RecursiveLinearSearch(3-1):T(n) = T(n−1) + O(1) —— 一个 T;
  • InsertionSort(3-2):T(n) = T(n−1) + S(n) —— 一个 T,嵌一个 S;
  • MergeSort(本课):T(n) = T(⌈n/2⌉) + T(⌊n/2⌋) + S(n) —— 两个 T,还嵌一个没解开的 S。
教授的收官悬念(逐字) "Even if we solve S, this is still a non-trivial task to figure out what is the big-O time complexity for T... And that will be the topic for Thursday."(就算 S 解出来了,从这条递推式里挖出 T 的 Big-O 仍然不容易——那是周四的主题。)于是本课在两个问号上落幕:S(n) = ?,T(n) = ?。理论暂时哑火——但「谁更快」的好奇心等不到周四,下一节先用实验解馋。
09

实验室:对决场 / Lab: MergeSort vs InsertionSort

同一份输入,两位选手,只数一件事:元素之间的比较次数。口径:InsertionSort 记 3-2 递归版 insert 的账(逐格比、不合适就换位),MergeSort 记本课 Merge 的账(每输出一个元素至多比一次)。曲线全部是数出来的,不是公式画的。

互动 · 对决场:比较次数计数器
逆序(IS 的 worst case) 随机打乱 已排序(IS 的 best case)
数组大小 n: 32
InsertionSort 0
MergeSort 0
InsertionSort(实测)MergeSort(实测)· 圆点 = 当前 n
实测战报(逆序,n = 256) InsertionSort:256 × 255 ÷ 2 = 32,640 次比较;MergeSort:1,024 次——差 32 倍。把 n 推到 1024(滑杆外,手算):523,776 对 5,120,102 倍,而且差距随 n 一路拉大。曲线的弯法肉眼可辨:一条奔着二次抛物线冲天,一条几乎贴地爬。这就是「切中间」买到的东西。
但请注意两个「诚实条款」 ① 把输入切到「已排序」:InsertionSort 只比 n−1 次(每个元素比一次就住手)——反超 MergeSort!L1-2 的老话应验:"best case is often considered cheating."(best case 算作弊。)给用户的承诺按 worst case 写,worst case 里 MergeSort 完胜。② 曲线是数出来的经验事实,不是定理——那个著名的名字,本课还没有资格说出口:式子还没解。周四之后,它才是定理。
10

四个经典陷阱 / Pitfalls

MergeSort 的坑,一半在 Merge 的细节里,一半在「以为已经学完了」的错觉里。对号入座。

坑一 · 「Merge 每层做两个递归调用」
S(n) = 2S(n−1) + O(1) ✗

伪代码里写了两处递归,但 if-else 每层只执行一个,总 size 恰好减 1。正确的账:S(n) = S(n−1) + O(1)。数递归调用永远数「执行了几个」。(真正每层执行两个的是 MergeSort 自己——所以 T 的式子里有两个 T。别把两笔账记串。)

坑二 · 拿 Merge 缝没排过序的数组
Merge([8,1], [4,2]) → [4,2,8,1] ✗

Merge 的前置条件是两输入各自有序。喂进乱序数组它照跑不误、绝不报错,然后输出胡话:比较 8 和 4 → 出 4;比较 8 和 2 → 出 2;Y 空,整段白送 [8,1] → 得 [4,2,8,1]。§03 三段论的第 ①② 条全靠前置条件撑着——地基一抽,大楼即塌。

坑三 · base case 只写 n == 0
if n == 0 then return A ✗

试试 n = 1:m = ⌈1/2⌉ = 1,于是 BL ← MergeSort(A[1..1])——和原问题一样大!违反 3-1 铁律,无限递归。讲义写的是 n ≤ 1:那个「≤」一个字符,接住了对半切永远切不小的最后一格。写分治算法,base case 永远要问一句:最小的递归入口是多大?

坑四 · 「今天起 MergeSort = O(n log n)」
把没证的结论写上答卷 ✗

本课的合法战利品只有递推式:S(n) = S(n−1) + O(1),T(n) = T(⌈n/2⌉) + T(⌊n/2⌋) + S(n)。递推式 ≠ 闭式解。§09 的曲线是实验证据,不是证明;§06 的「每层 × 层数」是味道,不是菜。答卷上敢写 O(n log n) 的那一天,从周四开始。

11

挑战题 / Harder problems

四道,难度递增。C1 就是讲义亲手发的那张考卷 (Exercise);C3 会把上一课整个装进口袋;C4 用上教授「k、ℓ 任意」的伏笔。先动笔再看解。

★★ · 讲义原题 (Exercise)
C1. 不看 §07,独立写出 MergeSort 正确性的完整归纳证明:三步齐全,并明确指出——归纳步里哪两处分别用到了「强归纳假设」和「Merge 定理」。

提示:先给两个递归调用的输入 size 做体检(⌈n/2⌉ 和 ⌊n/2⌋ 都严格小于 n 吗?什么时候?),再想拼装那一步凭什么正确。

题解 · Solution
Base n ≤ 1:数组本身即答案,第一行原样返回,正确。
IH(强) 对一切 size < n 的数组,MergeSort 返回其元素的升序重排。
Step n ≥ 2:子问题 size 为 ⌈n/2⌉ 与 ⌊n/2⌋,都 ≥ 1、都 ≤ n−1 —— 此处用强 IH(两个 size 都不是 n−1,弱假设覆盖不到):BL、BR 分别是前半、后半的升序重排。两半元素合计恰为 A 全体。此处用 Merge 定理:BL、BR 各自有序满足前置条件,故 Merge 返回全部 n 个元素的升序数组 = A 的升序重排。∎
评分点 ① 强假设的「for all sizes < n」措辞;② 体检 n ≥ 2 ⇒ ⌈n/2⌉ ≤ n−1(n=1 时不成立!全靠 base case 挡在前面);③ 归纳步引用 Merge 定理时点名前置条件已满足。三处缺一处,链条就有豁口。
★★★ · 把合并机的读数证成定理
C2. 设 X[1..k]、Y[1..ℓ] 各自有序,n = k + ℓ。证明:(a) Merge(X, Y) 至多做 n−1 次元素比较;(b) 存在输入恰好逼出 n−1 次;(c) 存在输入只需 min(k, ℓ) 次,且这是下限。

提示:每做一次比较,输出会发生什么?比较在什么时刻永远停止?§04 的场景 B 和场景 C 就是 (b)(c) 的原型。

题解 · Solution
(a) 上限 每次比较恰好送一个元素出列(输出长度 +1);而一旦某个数组被掏空,算法进入 base case,剩下的元素零比较整段白送——且此时至少还剩 1 个元素没输出。所以做比较送出的元素至多 n−1 个,即比较 ≤ n−1 次。∎
(b) 逼出 n−1 次 取完全交错的输入,如 X = [1,3,5,7]、Y = [2,4,6,8](场景 B):两边的头元素轮流获胜,谁也不提前掏空——直到只剩最后一个元素时才触发 base case,前 n−1 个元素每个都花了一次比较。一般构造:让 X 与 Y 交替占据全序的相邻位置即可。∎
(c) 下限 min(k, ℓ) 先证「≥ min(k,ℓ)」:比较停止的唯一方式是某个数组被掏空,而被掏空的那个数组的每个元素都是靠比较出列的(白送的只能是另一边的剩余),所以比较次数 ≥ 被掏空数组的 size ≥ min(k, ℓ)。再给达到下限的构造:让短的一边整体小于长的一边,如 X = [5,6,7,8]、Y = [1,2,3](场景 C):Y 的 3 个元素连胜三场后 Y 空,X 整段白送——恰好 min(k,ℓ) = 3 次。∎
品一品 这题把 §05 的「线性感」落了一半的地:比较次数被夹在 min(k,ℓ) 和 n−1 之间,怎么喂都是 Θ(n) 级的量。剩下那一半(把「每层常数杂务」也算进去、正式解出 S(n) = O(n))等周四。
★★★ · 把刀挪回去
C3. 把 MergeSort 第二行改成 m ← n−1(其余不动):
LopsidedSort(A[1..n])
if n ≤ 1 then return A
m ← n−1 // 刀挪到尾巴
BLLopsidedSort(A[1..m])
BRLopsidedSort(A[m+1..n])
return Merge(BL, BR)
(a) 它还合法吗(铁律体检)?还正确吗?(b) 这个算法其实是谁?(c) 写出它的 T(n) 递推式,与 3-2 的对比。

提示:BR 永远只有一个元素。用一个单元素数组去 Merge 一个有序数组,像不像某个老朋友的日常工作?

题解 · Solution
(a) 体检与正确性 n ≥ 2 时子问题 size 为 n−1 和 1,都 ≥ 1 且 < n ✓ 铁律满足;正确性证明与 §07 逐字相同(强 IH 覆盖 n−1 和 1,Merge 定理照用)。合法且正确——刀的位置不影响对错。
(b) 认人 BR = [A[n]] 是单元素数组。Merge(BL, [x]) 干的事:从左往右拿 x 和 BL 的头元素比,小的先出列——即在有序数组里为 x 找位置。这就是 insert!所以 LopsidedSort = 递归版 InsertionSort 换了件外套(细节差异:3-2 的 insert 从右端往左比,这里从左端往右比;单次比较数逐例不同,worst case 同为 n−1)。
(c) 递推式 T(n) = T(n−1) + T(1) + S(n) = T(n−1) + O(1) + S(n)——与 3-2 的 T(n) = T(n−1) + S(n) 同形(多出的 T(1) 是常数,不改形状)。
品一品 同一副骨架、同一个 Merge,只挪一个切点:递推式从「T(n−1) + 线性感」变成「两个 T(n/2) + 线性感」。MergeSort 的全部新意 = 刀的位置。这刀值多少钱,§09 已经偷看过实验答案;理论定价周四出炉。
★★★★ · 三分天下
C4. 设计三路归并排序 MergeSort3:把数组切成大致相等的三段,分别递归排序,再合并。(a) 只用本课的二路 Merge,写出合并三个有序数组的方法,并论证其合法性与正确性;(b) 给出合并步骤的比较次数上界(用三段长度 n₁、n₂、n₃ 表示,n = n₁+n₂+n₃);(c) 写出 MergeSort3 的 T₃(n) 递推式。它比二路更快吗?

提示:教授把 Merge 写成「k、ℓ 任意」的一般形式,伏笔正是为这种复用打的。(c) 的最后一问,想想本课的工具够不够回答。

题解 · Solution
(a) 复用二路 Merge Merge(Merge(B₁, B₂), B₃):先缝前两段,再把结果与第三段缝合。合法性:Merge 对任意长度 k、ℓ 的有序输入都已证正确(教授写一般形式的伏笔在此兑现)——第一次调用的输出有序,恰好满足第二次调用的前置条件。正确性:两次引用 Merge 定理即得。切点取 m₁ = ⌈n/3⌉、m₂ = m₁ + ⌈(n−m₁)/2⌉,三段 size 都 ≤ ⌈n/3⌉ + 1 级别、n ≥ 2 时都 < n,铁律体检可仿 §02 逐一验证。
(b) 比较次数 由 C2(a):第一次 Merge ≤ n₁ + n₂ − 1 次;第二次 ≤ (n₁+n₂) + n₃ − 1 = n − 1 次。合计 ≤ n₁ + 2n₂ + 2n₃ − 2 ≤ 2n − 2 次——仍是「线性感」,只是常数翻了倍。(顺带的观察:把最长的一段留到第二次再缝,上界更紧——合并顺序是有讲究的。)
(c) 递推式 T₃(n) = T₃(n₁) + T₃(n₂) + T₃(n₃) + (合并成本),粗记为 T₃(n) = 3·T₃(≈n/3) + (≤ 2n−2 次比较的线性感成本)。
更快吗?——诚实回答:本课的工具不够。 直觉两头拉扯:切三份,树矮了(log₃n 层 < log₂n 层);但每层的合并杂务贵了(≤ 2n−2 对 ≤ n−1)。谁赢?没有解递推式的工具,这个问题无法回答——这正是周四存在的意义。(剧透一寸:两者最后同档;层数省下的因子恰好被每层的开销吃回去。到 4-x 你会亲手算出这一点。)
12

随堂小测 / Quick Check

无限次尝试,零压力。七道题,把「切—缝—证—记账」这条链各敲一遍。

Q1. MergeSort 与递归版 InsertionSort 最本质的区别是?
Q2. n = 5 时,m = ⌈5/2⌉ = 3。MergeSort 切出的两半是?
Q3. 一次(非 base case 的)Merge 调用里,实际发生几个递归调用?
Q4. Merge([1,2,8,9], [4,5,6,8]) 全程做了几次元素比较?
Q5. 凭什么只比较一次,就敢把 X[1] 放上输出的第一位?
Q6. Merge 的递推式 S(n) = S(n−1) + O(1) 里,n 和 n−1 分别指什么?
Q7. 这节课结束时,关于 MergeSort 的时间复杂度,我们的合法结论是?