SICI · 天机门 · 第五章

算法观澜

效率的度量 · 炼气五层

给我一个循环,什么算不了——这句话,停机之渊记下了。

门楣刻字:「效率为尺」
序章

观澜台上 · 三令牌下注

挑一块「最好的」算法

观澜台悬在万法之上。台下,无数算法如光河流淌——有的湍急,有的缓滞,有的在暗处打转,永不前行。石碑刻着四个字:「效率为尺。」

观澜台上,台下无数算法光河流淌

你嗤笑。算法不就是代码写得快不快?给你一个 for 循环,什么算不了?台角站着一个人影,通体由打孔纸带编织而成——诺依曼老祖。他递过来三块令牌。

诺依曼老祖 诺依曼老祖

「挑一块最好的。」他的声音像机器咬合,没有任何温度。「你敢把直觉押上去吗?」

停机之渊 停机之渊

「最快的最好。」深渊在台下低语。「我替你记账。每一笔,S9 清算。」

三令牌下注 · 逐一执行,看各自的代价

三块令牌各刻一段伪代码。逐一执行——有的快但错,有的对但慢,有的对且快但吃内存。👆 点击「下注·执行」

—
一个返回错误答案的算法不是算法——它是一个带着热情的错误。「好」不是一个字,是三把尺:正确性、时间、空间。你刚才不是在挑令牌,你是在发现度量的维度。
详解 · K05-01 算法的概念与衡量尺度:正确性、时间、空间

算法 = 有限、确定、可行的指令序列,且必须终止。正确性是第一公民:对一切合法输入给出正确输出——大数溢出截断的"闪电法"再快也不是算法。时间与空间是第二、三把尺,二者常可互换:哈希表以空间换时间,归并排序用 O(n) 额外空间买 O(n log n) 的确定性——"时空权衡"是贯穿全部算法设计的母题。唯有正确性不可交易。衡量一个算法,永远三把尺一起拿出来。(CLRS Ch1)

第一幕 · 自信

渐近之尺 · 大O的诞生

常数不算数,增长率才是永恒判决

尺子入手冰凉。刻度不是数字,是函数。台下光河中浮出九个函数球——1、log₂n、√n、n、n log₂n、n²、n³、2ⁿ、n!——在无标度的虚空中乱作一团。

诺依曼老祖 诺依曼老祖

「把它们从小到大排好。」

停机之渊 停机之渊

「100n² 比 n³ 大得多,不是应该排前面?」

九函数排序 · 按增长率从小到大排到渐近之尺上

点击函数球,按增长率从小到大依次排入尺槽。排错会震动提示。

渐近之尺

1. —
2. —
3. —
4. —
5. —
6. —
7. —
8. —
9. —
九球排完后,三符号槽解锁——拖 n² 到三个槽,发现 O / Ω / Θ。

三符号槽 · O / Ω / Θ 的定义

O(上界)—
Ω(下界)—
Θ(紧界)—
Big-O 不问你今天跑多快——它问当问题长到无穷大时你会以什么姿势死。常数是今天的幻觉,增长率是永恒的判决。
详解 · K05-02 渐近记号:O / Ω / Θ 的定义与运算性质

形式定义:O(g) = { f(n) : ∃c,n₀>0,n≥n₀ 时 0 ≤ f(n) ≤ c·g(n) }——渐近上界;Ω(g) 把不等号反向——渐近下界;Θ(g) = O(g) ∩ Ω(g)——紧界。运算性质:常数因子与低阶项被吸收(100n²+n = Θ(n²)),且有对偶律 f=O(g) ⇔ g=Ω(f)。注意"f=O(g)"的等号是惯例滥用,应读作"f 属于 O(g)"——渐近记号描述的是函数集合的归属关系,不是等式。排序九函数的那把尺(1 < log n < √n < n < n log n < n² < n³ < 2ⁿ < n!)值得背到条件反射。(CLRS Ch3)

第一幕 · 巅峰

量劫仪 · 三种天命

同一把快排,三种命运

量劫仪是一座三面棱镜,内部悬浮着一段快排伪代码。

诺依曼老祖 诺依曼老祖

「快排。你给它什么,它就是什么命。」

你不信。快排不就是 O(n log n)?尺子上写着的。棱镜第一面亮了——你喂进去一个已排好序的数组。光河猛地滞住,像撞上一堵墙。

三面棱镜 · 喂不同输入,看快排的三种命运

点击下方按钮,逐一喂入三种输入,点亮三面棱镜。

最坏情形
—
平均情形
—
最好情形
—
逐一喂入三种输入,看三条曲线分叉。
你不分析算法——你分析它最倒霉的那天。因为用户不会承诺永远给你好输入,但你可以承诺,无论输入多糟,你的算法不会比「最坏」更差。
详解 · K05-03 时间/空间复杂度分析:最坏 / 平均 / 最好情形

同一个算法,复杂度是输入的函数:最坏情形给出"保证"(工程承诺的锚点),平均情形给出"期望"(必须声明输入分布假设),最好情形几乎没有工程意义。快排是最好的教材:已排序输入 + 固定 pivot 退化为 O(n²),随机输入期望 O(n log n)。随机化 pivot 的深意:把"倒霉的输入"转化为"倒霉的随机数"——期望复杂度从此与输入分布无关,敌人无法再针对性投喂最坏数据。分析空间复杂度同理(递归深度即栈空间,快排最坏 O(n) 栈)。(CLRS Ch3 / Ch7)

第一幕 · 崩塌

分形塔 · 递归的代价

T(n)=aT(n/b)+f(n)——合并代价才是账单

分形塔的每一层都是上一层的缩影——递归的物理化身。塔壁刻着一行递推方程:T(n) = aT(n/b) + f(n)。

诺依曼老祖 诺依曼老祖

「搭一棵递归树,看看每层花多少。」他递来一面镜子——递归之镜,镜框铭刻「映树」。

你心想:分治嘛,二分两半再合并,不就 O(n log n)?你在镜面上写下 T(n) = 2T(n/2) + n²——合并那一步是 n²,你以为无所谓。镜中的递归树长出来了。你数了数每层的总和——n² + n²/2 + n²/4 + …… = 2n²。不是 n log n。是 n²。塔壁裂了一道缝。

递归树 + Master 定理 · 四个递推归类

切换四个预设递推,看递归树每层总和,自动归类到 Master 三区域。

Master 定理三区域

Case 1 · 叶主导
f(n) < n^(log_b a)
Θ(n^(log_b a))
Case 2 · 均衡
f(n) = Θ(n^(log_b a))
Θ(n^(log_b a)·log n)
Case 3 · 根主导
f(n) > n^(log_b a)
Θ(f(n))
切换四个递推,全部归类后,切到「陷阱」做代入法验证。
Master 定理是一把判官尺:合并代价小,叶子说了算;合并代价大,根节点说了算;两者相等,每一层都得交税。你以为分治是免费的午餐——不,合并那一步才是账单。
详解 · K05-04 分治策略与递推方程:代入法 / 递归树 / Master 定理

分治递推 T(n) = aT(n/b) + f(n):a 个子问题、规模 n/b、合并代价 f(n)。三种解法各有分工:代入法先猜后用数学归纳法验证(本章陷阱递推取 c≥2 证明闭合);递归树逐层求和,几何级数的走向直接暴露主导项;Master 定理比较 f(n) 与分水岭 n^(log_b a):f 小→叶主导 Θ(n^(log_b a)),相等→每层均摊 Θ(n^(log_b a)·log n),f 大→根主导 Θ(f(n))(Case 3 还需正则条件 a·f(n/b) ≤ c·f(n))。Strassen 矩阵乘把 a 从 8 压到 7,Θ(n^2.807) 就此打败朴素 n³——分治的艺术全在"怎么切、怎么合"。(CLRS Ch4)

第二幕 · 震惊

排序刑场 · 不可逾越的下界

有些墙不是技术砌的,是数学砌的

排序刑场是一片开阔的石台。诺依曼老祖:「排好它们。比 O(n log n) 更快。」你不信邪。冒泡太慢?换快排。快排最坏 O(n²)?换堆排。堆排也是 O(n log n)?换归并——还是 O(n log n)。你试了五种排序,没有一种低于 n log n。

停机之渊 停机之渊

「换更聪明的排序,总能比 n log n 快——是吗?」

刑场中央升起一棵巨树——不是递归树,是判定树。每个节点是一次比较,每条路径是一种排列结果。

五排序竞速 + 判定树下界 · Ω(n log n)

拖 n 滑块看五条曲线全部卡在 n log n 以上;展开判定树数叶子;Stirling 展开确认下界。

10
冒泡 O(n²) 插入 O(n²) 归并 O(n log n) 快排 O(n log n) 堆排 O(n log n)

判定树论证

叶子数:— 树高:—
三步全做:竞速 + 判定树 + Stirling。
这不是在考验你的聪明。n! 种排列需要 log(n!) 次比较才能分辨,而 log(n!) = Ω(n log n)。你找不到更快的比较排序——不是因为你不够能干,是因为它不存在。
详解 · K05-05 排序算法谱系与比较排序下界 Ω(n log n)

排序谱系的取舍矩阵:插入/冒泡 O(n²) 但简单稳定;归并 O(n log n) 稳定、可外排,代价是 O(n) 空间;快排期望 O(n log n) 原地但不稳定;堆排 O(n log n) 原地但不稳定。比较排序下界:任何基于比较的排序对应一棵判定树,n! 种输入排列必须各占一片叶子 ⇒ 树高 ≥ log₂(n!),Stirling 展开得 Θ(n log n)——这是信息论下界,与聪明无关。要更快只有跳出"两两比较"模型:计数排序、基数排序、桶排序利用键值的额外结构可达 O(n),但它们不是通用排序。(CLRS Ch6–8)

第二幕 · 希望→裂痕

散列阁 · O(1)的幻象

O(1) 平均 ≠ O(1) 最坏

散列阁里悬着无数小格,每格有自己的编号。诺依曼老祖递给你一个哈希函数:「把数据丢进去,算个地址,直接放。不比较,不排序。」

你试了——O(1)。丢一个,查一个,都是 O(1)。你笑了:「这就是答案。排序下界?那是比较排序的下界。我不比较。」然后你丢进了第一百个数据。两个数据算出了同一个地址。格子里撞了。

哈希表模拟 · 冲突 + 摊还分析

点击插入数据,看冲突如何发生;拖入恶意输入看退化;展开摊还分析。

插入≥3次 + 触发冲突/恶意输入 + 展开摊还分析。
O(1) 平均不是 O(1) 最坏。摊还是对许多次操作的承诺,不是对任何一次操作的保证。代价没有消失——它被推迟了,攒在一次 rehash 里爆发。
详解 · K05-06 哈希表:冲突处理与平均 / 摊还分析

哈希表把"比较"换成"计算地址",期望 O(1)。冲突处理两大路线:链地址法(每格挂链表,平均查找 O(1+α),α=n/m 为装载因子)与开放寻址(线性/二次/双重哈希探查,期望探查数 ≈ 1/(1−α),α 必须 < 1)。平均 O(1) 的前提是全域哈希的均匀性假设——对手若能构造同哈希键(哈希洪水攻击),查找退化为 O(n),这正是真实世界 DoS 的手段。摊还分析:表倍增下插入均摊 O(1),因为 rehash 总代价是几何级数 m+2m+…+n < 2n;三种分析工具——聚合分析、记账法、势能法——是 CLRS Ch17 的三件套。(CLRS Ch11 / Ch17)

第二幕 · 再崩塌

贪心迷阵 · 局部最优的陷阱

贪心每步都对,结果全错

贪心迷阵的每条岔路口都写着「取最优者行」。第一关活动选择——每步选最早结束的,完美。第二关 Huffman 编码——每步合并频率最低的,完美。你笑了:「每步取最优,全局就最优。」

第三关——0-1 背包。容量 10,三件宝物:重 6 值 12、重 5 值 10、重 5 值 10。你按性价比选了最贵的——重 6 值 12。剩 4 的空间,什么都装不下了。总计值 12。

诺依曼老祖 诺依曼老祖

「另一条路:选两件重 5 值 10——总重 10,总值 20。你每步都选了最优。你的结果比最优差了 40%。」

贪心三关 · 活动选择 → Huffman → 0-1 背包

关卡1:点击活动条选最早结束的;关卡2:构建 Huffman 树;关卡3:看贪心翻车。

关卡1 · 活动选择(选最早结束)

逐一通过三关,看贪心在 0-1 背包上翻车。
贪心不是错的。贪心是瞎的。它在贪心选择性质成立时完美——那是问题的属性,不是算法的功劳。当问题没有这个属性时,贪心每步都对,结果全错。
详解 · K05-07 贪心算法思想:贪心选择性质与局部最优

贪心成立需要两根支柱同时立住:①最优子结构(最优解包含子问题最优解)②贪心选择性质(存在包含贪心选择的最优解——局部最优"安全")。活动选择(选最早结束,CLRS 16.1 原例 11 活动最优 4 个)与 Huffman 编码(合并最低频,可证前缀码最优)都满足②;0-1 背包满足①不满足②——选了重 6 值 12 就堵死两件重 5 值 10 的组合(12 vs 20);而分数背包(可切割)却满足②,贪心反而正确。验证贪心的标准武器是交换论证(exchange argument):证明任一最优解都能"换"进贪心选择而不变差——证明缺位时,贪心只是启发式。(CLRS Ch16)

第二幕 · 再崩塌

记忆之殿 · 重叠的代价

DP 治重复,不治规模

记忆之殿的墙壁上刻满了格子——每一个格子是一个子问题的答案。你在墙上写下递推:OPT(i, w) = max{ OPT(i-1, w), OPT(i-1, w-wᵢ) + vᵢ }。格子逐个填满,最优解 20 浮现。你笑了:「DP。天下无敌。」

诺依曼老祖 诺依曼老祖

递过递归之镜——第二次了。镜面铭刻已变:「映图」。「现在解这个。」他在镜上写下:TSP。

S 是已访问城市的子集。子集有多少?2ⁿ。10 个城市,1024。15 个,32768。20 个——一百万。墙壁裂了。

DP 表格填充 + TSP 状态爆炸

0-1 背包 DP(3 件物品,W=10)

TSP 状态爆炸 · 2ⁿ·n 计数器

5 城
状态数 2ⁿ·n = 160
递归之镜 · 映图——子问题 DAG 在镜中膨胀
填完 DP 表 + 引爆 TSP 状态爆炸。
DP 没有消灭指数——它只是把重叠的指数折成多项式。当子问题不重叠、状态本身指数膨胀时,DP 就是带了一张更大备忘录的暴力搜索。记忆只治「重复」,不治「规模」。
详解 · K05-08 动态规划思想:最优子结构与重叠子问题

DP 的两大适用条件:最优子结构(最优解由子问题最优解组装)+重叠子问题(子问题被反复求解,值得记忆)。实现两路线:自顶向下备忘录(递归+缓存)与自底向上填表(按拓扑序迭代)。0-1 背包递推 OPT(i,w) = max{ OPT(i-1,w), OPT(i-1,w-wᵢ)+vᵢ } 填出最优 20,复杂度 O(nW) 是伪多项式——W 按输入位长算是指数级。TSP 的 Held-Karp DP 状态为(已访问子集, 当前城市),2ⁿ·n 个状态:子问题确实重叠,但状态空间本身指数膨胀,记忆也救不了。DP 的能力边界 = 子问题 DAG 的规模。(CLRS Ch15)

第三幕 · 重燃→逼近

迷图深渊 · 路径与遍历

BFS 水波,DFS 藤蔓,Dijkstra 贪心,Bellman-Ford 补救

迷图深渊是一张巨网——节点如星辰,边如暗河。你吐出一口气。BFS——队列推进,一层一层扫开。DFS——深入到底再回溯。两种走法,O(V+E)。掌控感回来了。

你开始找最短路径——Dijkstra,贪心地取最近节点。干净利落。然后,一条边亮起了暗红色。它的权重是负的。Dijkstra 的光河在那条边前停住了。

诺依曼老祖 诺依曼老祖

「慢一点。Bellman-Ford。什么路都走得通。」

你以 O(VE) 的代价走完了负权图。但当你走到最深的那个节点——脚下的网碎了。下面是深渊。深渊里传来一个声音:「它会停吗?」

图遍历 + 最短路径 · BFS/DFS/Dijkstra/Bellman-Ford

试 BFS/DFS + Dijkstra(非负图)+ 注入负权后试 Dijkstra 失败 + Bellman-Ford 补救。
每个算法都有自己的领地。Dijkstra 在非负图上又快又准——因为它的贪心假设在非负权下成立。Bellman-Ford 在负权图上也能走——但代价是 O(VE)。没有免费的午餐,只有不同的权衡。
详解 · K05-09 图的遍历:BFS / DFS 及其应用

BFS 与 DFS 同为 O(V+E),是图算法的两大母结构。BFS(队列,按层推进):天然给出无权图最短路径,还可做二分图判定、层序分析。DFS(栈/递归,深入回溯):拓扑排序、连通/强连通分量、环检测的基石——发现时间 d 与完成时间 f 构成括号化区间(括号化定理),边的分类(树边/回边/前向边/横边)全部由此读出。选 BFS 还是 DFS,取决于你要"最近的答案"还是"最深的结构"。(CLRS Ch22)

详解 · K05-10 最短路径算法:Dijkstra 与 Bellman-Ford

Dijkstra:贪心 extract-min + 边松弛,堆实现 O((V+E) log V)。正确性论证的关键一步是"节点出队即封盘"——这依赖非负权假设;负权边可以让已封盘的距离变得更小,论证当场击穿,这正是注入负权边后 Dijkstra 给错答案的原因。Bellman-Ford:放弃贪心,对全部边做 V-1 轮松弛,O(VE) 换负权容忍;第 V 轮仍能松弛 ⇒ 存在负权环(本章"检测负权环"按钮即此原理)。选型口诀:非负单源 Dijkstra,负权单源 Bellman-Ford,全源 Floyd-Warshall O(V³),稀疏图全源 Johnson。(CLRS Ch24)

终幕 · 对决

停机之渊 · 三问排除法收网

有些问题不是慢——是不存在答案

深渊没有底。它不是黑暗——它是「无法判断」本身。它开口了,声音像无数程序同时在执行又同时卡死:

停机之渊 停机之渊

「你以为更快的算法能解决一切。我来告诉你——有些问题,不是慢。是不存在答案。三问。答得出来,我退。答不出来——你留在渊底。」

诺依曼老祖 诺依曼老祖

递出递归之镜——第三次,铭文已变:「映己」。「你的刻痕,够用了。」

渊账累计 0 笔 · 三问全对后清零

尾声

算法观澜 · 计算的边界

炼气五层 · 算法观澜 · 圆满

观澜台回到脚下。你低头看——石板上的五道刻痕亮了。五道刻痕拼成一幅地图。不是迷路的图——是边境的图。

诺依曼老祖 诺依曼老祖

「你来了的时候说:给我一个循环,什么算不了。现在你知道了。有些东西,算不了。不是因为你不够能干——是因为它们不在计算的范围里。这不是退步。这是你第一次看见了世界的形状。」

知识图谱点亮 · 十二碎片排认知链

点击下方碎片,按因果顺序点亮认知链。

排错处会闪烁红光,提示该知识点依赖的前置知识。

五刻痕回顾 · 触碰刻痕,回闪对应场景

刻痕① 递归无代价 → Master 定理
刻痕② 比较排序破 n log n → 判定树下界
刻痕③ 贪心万能 → 0-1 背包反例
刻痕④ DP 万能 → TSP 状态爆炸
刻痕⑤ 计算万能 → 停机不可判定

递归之镜三阶段

映树(S3)· 递归树
映图(S7)· 子问题图
映己(S9)· 自指悖论

三层计算边界地图

知止而后有定
衡量一个计算机科学家的,不是他解决了多少问题——而是他多精确地知道,哪些问题可以被解决。算法是地图——武器有极限,地图告诉你极限在哪。这就是观澜。知止而后有定——边界不是牢笼,是定义。
详解 · K05-11/12 P/NP/NP 完全与不可计算性:三层边界复盘

P = 多项式时间可解;NP = 多项式时间可验证(给证书能核验);NP 完全 = NP 中"最难"的一批——Cook-Levin 定理证明 SAT 是第一个 NP 完全问题,此后一切 NP 完全结论都靠多项式归约接力(SAT → 3-SAT → 团 → 顶点覆盖 → TSP…)。P=NP?千禧年难题,悬而未决——但多数研究者押 P≠NP。不可计算是更深的墙:停机问题不可判定,对角化构造 D(D) 让任何候选判定器 H 自指矛盾(Turing 1936),且由 Rice 定理,程序的任何非平凡语义性质都不可判定。三层边界收束本章:效率边界 Ω(n log n)(定理砌的)→ 复杂性边界 NP 完全(可能砌的)→ 可计算性边界停机(必然砌的)。下一章 ch06 硅基咒语:从算法的抽象世界回到机器——用硬件描述语言把逻辑钉进硅片。(CLRS Ch34 / Sipser Ch4/7)

ch06 硅基咒语 🔒(点亮全部图谱后开启)
交互实验

算法复杂度对比

拖动最大 n,对比 O(1)/O(log n)/O(n)/O(n log n)/O(n²) 的增长差距。