算法分析基础与学习路线

108 minBeginner2026/7/20

算法分析(Algorithm Analysis)的形式化定义、五类渐近记号(Bachmann 1894《Analytische Zahlentheorie》大 O 符号、Landau 1909《Handbuch》推广、Knuth 1976《Big Omicron and Big Omega and Big Theta》SIGACT News 12(3):36-44 系统化)、计算复杂性类(Hartmanis-Stearns 1965《On the Computational Complexity of Algorithms》Trans. AMS 117:285-306、Cobham 1964/Edmonds 1965 P 类、Cook 1971《The Complexity of Theorem-Proving Procedures》STOC、Karp 1972《Reducibility Among Combinatorial Problems》STOC 197-206)、主定理(Bentley-Haken-Saxe 1980 SIGACT News 12(3):36-44)、摊还分析(Sleator-Tarjan 1985《Amortized Efficiency of List Update and Paging Rules》CACM 28(2):202-208)、随机化分析(Rabin 1976)、时空权衡策略与系统化学习路线图,涵盖 Turing 1936《On Computable Numbers》Proc. LMS 42:230-265、Knuth 1968 TAOCP Vol.1、Cormen-Leiserson-Rivest 1990《Introduction to Algorithms》第 1 版的历史脉络,附 Python/C++/Java 多语言实现与 CLRS 第 1-4 章、Kleinberg-Tardos 第 2-5 章、Sedgewick 第 1 章风格习题。

1. 概述与学习目标

1.1 什么是算法分析

算法分析(Algorithm Analysis)是研究算法所需资源(时间、空间、I/O、能量)与输入规模之间定量关系的学科。Donald E. Knuth 1968 在《The Art of Computer Programming, Volume 1: Fundamental Algorithms》第 1.2.10 节系统化算法分析为现代学科,并引入渐近记号的标准用法。算法分析的目标不是”算出运行时间的精确秒数”,而是回答以下四类核心问题:

  1. 时间复杂度:算法在规模为 nn 的输入上需要多少基本操作?
  2. 空间复杂度:算法需要多少额外存储?
  3. 正确性:算法对所有合法输入是否产生正确输出?
  4. 最优性:是否存在比该算法更优的算法?
算法分析层次模型:
                                算法分析
                                    |
        ┌───────────┬───────────────┴───────────────┬───────────┐
      时间分析    空间分析         正确性分析          最优性分析
        │            │                 │                  │
   ┌────┴────┐   ┌───┴───┐       ┌─────┴─────┐      ┌─────┴─────┐
  最坏  平均  最好  辅助   总空间    不变式    前后置    下界证明    上界证明
   摊还  期望  概率  空间             归纳法    条件     (Ω)      (O)

算法分析的五类渐近记号(Asymptotic Notation):

记号名称含义直觉
O(g(n))O(g(n))大 O(上界)最坏情况保证”不超过 g(n)g(n) 增长率”
Ω(g(n))\Omega(g(n))大 Omega(下界)固有难度”至少为 g(n)g(n) 增长率”
Θ(g(n))\Theta(g(n))大 Theta(紧界)精确阶”与 g(n)g(n) 同阶”
o(g(n))o(g(n))小 o(非紧上界)严格小于”低于 g(n)g(n) 增长率”
ω(g(n))\omega(g(n))小 omega(非紧下界)严格大于”高于 g(n)g(n) 增长率”

1.2 算法与问题

算法(Algorithm)是一组有限的、明确定义的指令序列,用于解决某类问题或完成某项计算任务。一个有效的算法必须满足以下五个基本性质(Knuth 1968 TAOCP Vol.1 §1.1):

性质含义反例
有穷性算法在有限步骤后必须终止死循环不构成算法
确定性每一步的执行含义唯一确定”将 x 加 1 或加 2” 不是确定性步骤
可行性每一步都能在有限时间内完成”计算精确的 π 值” 不可行
输入有零个或多个外部输入
输出有一个或多个输出

判定问题(Decision Problem):回答”是”或”否”。例如:给定图 GG 和整数 kkGG 中是否存在大小为 kk 的团?

优化问题(Optimization Problem):在所有可行解中寻找最优解。例如:给定图 GG,求最大团的大小。

两者之间存在深刻联系:优化问题通常可以转化为判定问题(通过二分答案),而判定问题的多项式时间算法往往能导出优化问题的算法。P vs NP 问题正是围绕判定问题的可解性展开的(Cook 1971, Karp 1972)。

1.3 问题规模与输入编码

问题规模 nn 是衡量输入大小的标尺,其选择直接影响复杂度函数的形式:

  • 数组排序:nn = 数组长度
  • 矩阵乘法:nn = 矩阵维度
  • 图算法:nn 可以是顶点数 V|V| 或边数 E|E|
  • 整数运算:nn = 整数的二进制位数(注意:nn \neq 整数本身的值)

编码方式的选择也很关键。同一个整数,用一元编码(nn 个 1)和二进制编码(logn\log n 位)表示,会导致完全不同的复杂度结论。在标准计算模型(图灵机/RAM)下,默认使用二进制编码。一元编码下的多项式时间算法,在二进制编码下变为伪多项式(pseudo-polynomial)时间,如 0-1 背包的 O(nW)O(nW) DP(WW 为背包容量,二进制编码下 WW 可达 2n2^n)。

跨模块引用:整数编码与位运算密切相关,参见 C++ 基础 中的位运算章节。

1.4 学习目标

完成本章学习后,读者应能够:

  1. 记忆(Remember):五类渐近记号的形式化定义、主定理三种情况、摊还分析三方法
  2. 理解(Understand):算法分析的历史脉络(Turing 1936 → Bachmann 1894 → Knuth 1976 → Hartmanis-Stearns 1965 → Cook 1971 → Karp 1972)
  3. 应用(Apply):使用主定理、代入法、递归树法求解递推关系,使用势能法分析数据结构摊还代价
  4. 分析(Analyze):归并/快速/堆/计数排序、Karatsuba/Strassen/FFT、二分查找、动态数组的复杂度证明
  5. 评估(Evaluate):各算法在时间/空间/稳定性/原地性/在线性/随机化维度上的优劣
  6. 对比(Compare):分治/贪心/动态规划/回溯/分支限界五大范式
  7. 创造(Create):设计基于算法分析的开源项目(可视化平台、复杂度分析工具、刷题追踪系统)

2. 历史动机与演进

2.1 计算理论的奠基(1930s)

Alan Turing 1936 在《On Computable Numbers, with an Application to the Entscheidungsproblem》(Proc. London Math. Soc. s2-42(1):230-265, DOI:10.1112/plms/s2-42.1.230)中提出图灵机(Turing Machine)作为通用计算模型,并证明停机问题不可判定。该论文解决了 Hilbert 1928 提出的”判定问题”(Entscheidungsproblem):是否存在算法判定任意一阶逻辑公式是否可证——Turing 给出否定回答。Church 1936 同时用 λ-演算独立得到相同结论,故称 Church-Turing 论题:任何”有效计算”均可由图灵机完成。

图灵机定义(简化版):

M=(Q,Σ,Γ,δ,q0,qaccept,qreject)M = (Q, \Sigma, \Gamma, \delta, q_0, q_{\text{accept}}, q_{\text{reject}})

其中 QQ 为状态集,Σ\Sigma 为输入字母表,Γ\Gamma 为带字母表(ΣΓ\Sigma \subseteq \Gamma),δ:Q×ΓQ×Γ×{L,R}\delta: Q \times \Gamma \to Q \times \Gamma \times \{L, R\} 为转移函数,q0q_0 为初始状态。

2.2 渐近记号的起源(1894-1976)

渐近记号的历史横跨数论到计算机科学:

年份人物事件
1894Paul Bachmann《Analytische Zahlentheorie》卷二 401 页首次使用大 O 符号(Ordnung von)
1909Edmund Landau《Handbuch der Lehre von der Verteilung der Primzahlen》卷二 883 页推广并标准化
1914G. H. Hardy & J. E. Littlewood引入大 Ω\Omega 与小 oo 符号至解析数论
1924Edmund Landau引入小 ω\omega 符号
1968Donald E. Knuth《TAOCP Vol.1》将渐近记号引入计算机科学
1970Donald E. Knuth引入大 Θ\Theta 符号表示紧界
1976Donald E. Knuth《Big Omicron and Big Omega and Big Theta》SIGACT News 8(2):18-24 系统化

Knuth 1976 论文的关键贡献:

  1. 澄清 OO vs Θ\Theta 的区别:程序员误用 OO 表示”紧界”(实际应为 Θ\Theta
  2. 推广 Ω\OmegaΘ\Theta:使三者构成完整的上/下/紧界体系
  3. 建议读法OO 读作 “big-Omicron”(大 Omicron),Ω\Omega 读作 “big-Omega”,Θ\Theta 读作 “big-Theta”
  4. 形式化定义:与 Bachmann-Landau 一致,强调 OO 是函数集合而非函数关系

2.3 计算复杂性理论的诞生(1960s)

Hartmanis-Stearns 1965 在《On the Computational Complexity of Algorithms》(Trans. AMS 117:285-306, DOI:10.1090/S0002-9947-1965-0170805-7)中系统化定义计算复杂性理论。关键贡献:

  1. 多带图灵机模型:作为复杂度分析的统一模型
  2. 时间复杂性类TIME(f(n))\text{TIME}(f(n)) = 可在 O(f(n))O(f(n)) 时间内被图灵机求解的判定问题集合
  3. 时间层次定理TIME(f(n))TIME(f(n)logf(n))\text{TIME}(f(n)) \subsetneq \text{TIME}(f(n) \log f(n))
  4. 复杂性类的相对性:递归函数按时间复杂度形成严格层级

两人因此获 1993 年 Turing Award。

Cobham 1964 在《The intrinsic computational difficulty of functions》中独立定义 P 类(多项式时间)。Edmonds 1965 在《Paths, trees, and flowers》(Canad. J. Math. 17:449-467, DOI:10.4153/CJM-1965-045-4)中称多项式时间算法为”好算法”(good algorithm)。Cobham-Edmonds 论题:P 类是”易解”(tractable)问题的形式化。

2.4 NP 完全性的发现(1971-1972)

Cook 1971 在 STOC《The Complexity of Theorem-Proving Procedures》(pp. 151-158, DOI:10.1145/800157.805047)中证明 SAT 是 NP 完全的(Cook-Levin 定理,Levin 1973 独立证明)。这意味着:

  • SAT ∈ NP
  • LNPL \in \text{NP}, LpSATL \leq_p \text{SAT}(多项式时间归约)

Cook 因此获 1982 年 Turing Award。

Karp 1972 在《Reducibility Among Combinatorial Problems》(Proc. IBM Symposium on Complexity of Computer Computations, pp. 85-103)证明 21 个经典组合问题均为 NP 完全,包括:

  • 3-SAT
  • 顶点覆盖(Vertex Cover)
  • 团(Clique)
  • 集合覆盖(Set Cover)
  • 哈密尔顿回路(Hamiltonian Cycle)
  • TSP 判定
  • 子集和(Subset Sum)
  • 背包判定
  • 图着色(Graph Coloring)
  • 3-维匹配(3-Dimensional Matching)

Karp 因此获 1985 年 Turing Award。P vs NP 千禧年七大难题之首(Clay Mathematics Institute Millennium Prize Problems, 2000),悬赏 100 万美元。

2.5 算法设计范式的发展

范式年代代表人物关键贡献
分治1945von NeumannEDVAC 报告描述归并排序
动态规划1952Bellman《On the Theory of Dynamic Programming》PNAS 38(8):716-719
贪心1956Kruskal / Prim最小生成树算法
回溯1965Golomb-Baumert《Backtrack Programming》JACM 12(4):516-524
分支限界1960Land-Doig《An automatic method of solving discrete programming problems》Econometrica 28(3):497-520
随机化1976Rabin《Probabilistic algorithms》引入随机化算法
近似算法1966GrahamList scheduling approximation
在线算法1985Sleator-Tarjan竞争分析

2.6 摊还分析的兴起(1980s)

Sleator-Tarjan 1985 在《Amortized Efficiency of List Update and Paging Rules》(CACM 28(2):202-208, DOI:10.1145/2786.2793)中系统化摊还分析(Amortored Analysis):

  1. 聚合分析(Aggregate Method):求 nn 个操作总代价 T(n)T(n),摊还代价 =T(n)/n= T(n)/n
  2. 核算方法(Accounting Method):为不同操作赋予不同”摊还代价”,多余的存为”信用”
  3. 势能方法(Potential Method):定义势能函数 Φ(D)\Phi(D),摊还代价 c^i=ci+Φ(Di)Φ(Di1)\hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1})

Tarjan 1985 在《Amortized computational complexity》(SIAM J. Alg. Disc. Meth. 6(2):306-318, DOI:10.1137/0606031)进一步系统化,应用于 Splay 树(Sleator-Tarjan 1985)、并查集(Tarjan 1975)、二项堆等。Sleator-Tarjan 因此获 1986 年 Turing Award。

2.7 主定理的形式化(1980)

Bentley-Haken-Saxe 1980 在《A general method for solving divide-and-conquer recurrences》(SIGACT News 12(3):36-44, DOI:10.1145/1008861.1008865)中系统化求解 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n) 形式递推。该论文将递归树方法形式化,给出三种情况分类(基于 f(n)f(n)nlogban^{\log_b a} 的渐近比较)。Cormen-Leiserson-Rivest《CLRS》第 4 章将其命名为 Master Theorem(主定理)。

2.8 教材的奠基(1968-1990)

  • Knuth 1968 《TAOCP Vol.1 Fundamental Algorithms》:算法分析的开山之作
  • Aho-Hopcroft-Ullman 1974 《The Design and Analysis of Computer Algorithms》:Addison-Wesley,引入算法分析的标准教学体系
  • Cormen-Leiserson-Rivest 1990 《Introduction to Algorithms》第 1 版(CLRS 第 1 版,2009 第 3 版加入 Stein,2022 第 4 版):MIT 6.006 课程教材,全球最广泛使用的算法教材
  • Sedgewick 1983 《Algorithms》第 1 版(2011 第 4 版与 Wayne 合著):Princeton 课程教材,以图示与代码见长
  • Kleinberg-Tardos 2006 《Algorithm Design》:Cornell 课程教材,以算法设计范式见长
  • Skiena 1998 《The Algorithm Design Manual》第 1 版(2020 第 3 版):Stony Brook 课程教材,以实战与”算法设计手册”风格见长

2.9 关键人物与里程碑

timeline
    title 算法分析关键里程碑
    1894 : Bachmann 大 O 符号
    1909 : Landau 推广渐近记号
    1936 : Turing 图灵机 / Church λ-演算
    1945 : von Neumann 归并排序
    1952 : Bellman 动态规划
    1960 : Land-Doig 分支限界
    1964 : Cobham P 类定义
    1965 : Hartmanis-Stearns 复杂性理论 / Edmonds P 类 / Golomb-Baumert 回溯
    1968 : Knuth TAOCP Vol.1 算法分析系统化
    1971 : Cook-Levin 定理 SAT NP 完全
    1972 : Karp 21 个 NP 完全问题 / Tarjan DFS 线性时间
    1976 : Knuth 渐近记号系统化 / Rabin 随机化算法
    1980 : Bentley-Haken-Saxe 主定理
    1985 : Sleator-Tarjan 摊还分析
    1990 : CLRS 第 1 版
    2000 : P vs NP 列入千禧年难题
    2022 : CLRS 第 4 版

2.10 关键设计决策

  1. 图灵机作为统一计算模型(Turing 1936):使复杂度分析独立于具体硬件
  2. 二进制编码默认:使整数复杂度反映输入位数而非数值
  3. 渐近记号而非精确常数:忽略硬件相关常数因子,聚焦算法本质
  4. 最坏情况分析为主:保证算法对所有输入成立
  5. 多项式时间作为”易解”判据(Cobham-Edmonds):P 类的哲学基础
  6. 摊还分析作为补充(Sleator-Tarjan 1985):对数据结构操作序列给出更紧的界

3. 形式化定义

3.1 五类渐近记号的形式化定义

定义 3.1(大 O 符号):设 f,g:NR0f, g: \mathbb{N} \to \mathbb{R}_{\geq 0},称 f(n)=O(g(n))f(n) = O(g(n)) 当且仅当

c>0,n0N,nn0:0f(n)cg(n)\exists c > 0, \exists n_0 \in \mathbb{N}, \forall n \geq n_0: 0 \leq f(n) \leq c \cdot g(n)

含义:f(n)f(n) 的增长率不超过 g(n)g(n) 的常数倍,表示算法的最坏情况上界

定义 3.2(大 Omega 符号):称 f(n)=Ω(g(n))f(n) = \Omega(g(n)) 当且仅当

c>0,n0N,nn0:0cg(n)f(n)\exists c > 0, \exists n_0 \in \mathbb{N}, \forall n \geq n_0: 0 \leq c \cdot g(n) \leq f(n)

含义:f(n)f(n) 的增长率至少为 g(n)g(n) 的常数倍,表示问题的固有难度下界

定义 3.3(大 Theta 符号):称 f(n)=Θ(g(n))f(n) = \Theta(g(n)) 当且仅当

f(n)=O(g(n)) 且 f(n)=Ω(g(n))f(n) = O(g(n)) \text{ 且 } f(n) = \Omega(g(n))

c1,c2>0,n0,nn0:c1g(n)f(n)c2g(n)\exists c_1, c_2 > 0, \exists n_0, \forall n \geq n_0: c_1 \cdot g(n) \leq f(n) \leq c_2 \cdot g(n)

含义:f(n)f(n)g(n)g(n) 同阶增长,是最精确的复杂度描述。

定义 3.4(小 o 符号):称 f(n)=o(g(n))f(n) = o(g(n)) 当且仅当

c>0,n0N,nn0:0f(n)<cg(n)\forall c > 0, \exists n_0 \in \mathbb{N}, \forall n \geq n_0: 0 \leq f(n) < c \cdot g(n)

等价定义:limnf(n)g(n)=0\lim_{n \to \infty} \frac{f(n)}{g(n)} = 0

含义:f(n)f(n) 的增长率严格小于 g(n)g(n)。例如 n=o(nlogn)n = o(n \log n)

定义 3.5(小 omega 符号):称 f(n)=ω(g(n))f(n) = \omega(g(n)) 当且仅当

c>0,n0N,nn0:0cg(n)<f(n)\forall c > 0, \exists n_0 \in \mathbb{N}, \forall n \geq n_0: 0 \leq c \cdot g(n) < f(n)

等价定义:limnf(n)g(n)=+\lim_{n \to \infty} \frac{f(n)}{g(n)} = +\infty

含义:f(n)f(n) 的增长率严格大于 g(n)g(n)。例如 nlogn=ω(n)n \log n = \omega(n)

3.2 渐近记号的运算性质

定理 3.1(渐近记号的运算性质):

  1. 传递性:若 f=O(g)f = O(g)g=O(h)g = O(h),则 f=O(h)f = O(h)
  2. 自反性f=O(f)f = O(f)
  3. 对称性f=Θ(g)f = \Theta(g) 当且仅当 g=Θ(f)g = \Theta(f)
  4. 转置对称性f=O(g)f = O(g) 当且仅当 g=Ω(f)g = \Omega(f)
  5. 加法规则O(f)+O(g)=O(max(f,g))O(f) + O(g) = O(\max(f, g))
  6. 乘法规则O(f)O(g)=O(fg)O(f) \cdot O(g) = O(f \cdot g)

证明(加法规则):设 f1=O(g1),f2=O(g2)f_1 = O(g_1), f_2 = O(g_2),则 c1,c2,n1,n2\exists c_1, c_2, n_1, n_2

nn1:f1(n)c1g1(n),nn2:f2(n)c2g2(n)\forall n \geq n_1: f_1(n) \leq c_1 g_1(n), \quad \forall n \geq n_2: f_2(n) \leq c_2 g_2(n)

n0=max(n1,n2),c=c1+c2n_0 = \max(n_1, n_2), c = c_1 + c_2,则 nn0\forall n \geq n_0

f1(n)+f2(n)c1g1(n)+c2g2(n)cmax(g1(n),g2(n))f_1(n) + f_2(n) \leq c_1 g_1(n) + c_2 g_2(n) \leq c \cdot \max(g_1(n), g_2(n))

f1+f2=O(max(g1,g2))f_1 + f_2 = O(\max(g_1, g_2))\blacksquare

3.3 常见复杂度等级

从快到慢排列(增长率单调递增):

O(1)O(loglogn)O(logn)O(n)O(n)O(nlogn)O(n2)O(n3)O(2n)O(n!)O(nn)O(1) \prec O(\log \log n) \prec O(\log n) \prec O(\sqrt{n}) \prec O(n) \prec O(n \log n) \prec O(n^2) \prec O(n^3) \prec O(2^n) \prec O(n!) \prec O(n^n)

实际意义:假设每秒执行 10910^9 次运算:

复杂度n=10n=10n=100n=100n=1000n=1000n=106n=10^6
O(logn)O(\log n)<1ns<1\text{ns}<1ns<1\text{ns}<1ns<1\text{ns}20ns20\text{ns}
O(n)O(n)10ns10\text{ns}100ns100\text{ns}1μs1\mu\text{s}1ms1\text{ms}
O(nlogn)O(n \log n)33ns33\text{ns}664ns664\text{ns}10μs10\mu\text{s}20ms20\text{ms}
O(n2)O(n^2)100ns100\text{ns}10μs10\mu\text{s}1ms1\text{ms}17min17\text{min}
O(2n)O(2^n)1μs1\mu\text{s}4×1013yr4 \times 10^{13}\text{yr}

3.4 计算模型

图灵机(Turing Machine, Turing 1936):

M=(Q,Σ,Γ,δ,q0,qaccept,qreject)M = (Q, \Sigma, \Gamma, \delta, q_0, q_{\text{accept}}, q_{\text{reject}})

  • QQ:有限状态集
  • Σ\Sigma:输入字母表(不含空白符 \sqcup
  • Γ\Gamma:带字母表(Σ{}Γ\Sigma \cup \{\sqcup\} \subseteq \Gamma
  • δ:Q×ΓQ×Γ×{L,R}\delta: Q \times \Gamma \to Q \times \Gamma \times \{L, R\}:转移函数
  • q0Qq_0 \in Q:初始状态
  • qaccept,qrejectQq_{\text{accept}}, q_{\text{reject}} \in Q:接受/拒绝状态

RAM 模型(Random Access Machine):算法分析的事实标准模型,假设:

  1. 每条基本指令(算术、比较、赋值、跳转、内存访问)耗时 O(1)O(1)
  2. 内存无限大且随机访问
  3. 每个字(word)可存储 O(logn)O(\log n) 位整数(nn 为输入规模)

Church-Turing 论题:任何”有效计算”均可被图灵机模拟。Cobham-Edmonds 论题:任何”易解”问题均可被多项式时间图灵机求解。

3.5 复杂性类的形式化定义

定义 3.6(P 类):

P=k=1TIME(nk)\text{P} = \bigcup_{k=1}^{\infty} \text{TIME}(n^k)

即所有可在多项式时间内被确定图灵机求解的判定问题集合。

定义 3.7(NP 类):

NP=k=1NTIME(nk)\text{NP} = \bigcup_{k=1}^{\infty} \text{NTIME}(n^k)

即所有可在多项式时间内被非确定图灵机求解的判定问题集合。等价定义:存在多项式 pp 与多项式时间验证器 VV,使得

xL    w,wp(x),V(x,w)=1x \in L \iff \exists w, |w| \leq p(|x|), V(x, w) = 1

定义 3.8(NP 完全):LL 是 NP 完全的当且仅当

  1. LNPL \in \text{NP}
  2. LNP:LpL\forall L' \in \text{NP}: L' \leq_p L(多项式时间归约)

定义 3.9(EXP 类):

EXP=k=1TIME(2nk)\text{EXP} = \bigcup_{k=1}^{\infty} \text{TIME}(2^{n^k})

已知包含关系:

PNPPSPACEEXP\text{P} \subseteq \text{NP} \subseteq \text{PSPACE} \subseteq \text{EXP}

由时间层次定理(Hartmanis-Stearns 1965),PEXP\text{P} \subsetneq \text{EXP}。但 P\text{P} vs NP\text{NP} 仍为开放问题。

3.6 递推关系的形式化定义

定义 3.10(递推关系):形如

T(n)=aT(n/b)+f(n),a1,b>1T(n) = a \cdot T(n/b) + f(n), \quad a \geq 1, b > 1

的递推关系,称为分治递推。其中 aa 为子问题数,n/bn/b 为子问题规模,f(n)f(n) 为分解与合并代价。

主定理(Master Theorem, Bentley-Haken-Saxe 1980):

对于 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n),记 nlogban^{\log_b a} 为”叶子工作量”。设 f(n)f(n) 渐近正,则:

情形 1:若 f(n)=O(nlogbaϵ)f(n) = O(n^{\log_b a - \epsilon})(对某个 ϵ>0\epsilon > 0),则 T(n)=Θ(nlogba)T(n) = \Theta(n^{\log_b a})

直觉:叶子节点总工作量支配根节点工作量。

情形 2:若 f(n)=Θ(nlogbalogkn)f(n) = \Theta(n^{\log_b a} \log^k n)k0k \geq 0),则 T(n)=Θ(nlogbalogk+1n)T(n) = \Theta(n^{\log_b a} \log^{k+1} n)

直觉:各层工作量大致相等。

情形 3:若 f(n)=Ω(nlogba+ϵ)f(n) = \Omega(n^{\log_b a + \epsilon})(对某个 ϵ>0\epsilon > 0),且正则条件 af(n/b)cf(n)a \cdot f(n/b) \leq c \cdot f(n)(对某个 c<1c < 1 与足够大的 nn),则 T(n)=Θ(f(n))T(n) = \Theta(f(n))

直觉:根节点工作量支配叶子节点总工作量。


4. 时空权衡

4.1 核心思想

算法设计中,时间与空间往往此消彼长。不存在同时达到时间最优和空间最优的”免费午餐”。时空权衡的核心策略有两类:

以空间换时间:使用额外的存储空间来加速计算。这是更常见的策略,因为内存成本持续下降而时间效率始终是核心瓶颈。

以时间换空间:在存储资源受限时,通过重复计算来减少存储需求。典型于嵌入式系统和大规模数据处理。

4.2 经典权衡案例

案例 1:排序中的辅助数组

归并排序需要 O(n)O(n) 额外空间来实现合并操作,但保证了 O(nlogn)O(n \log n) 的时间复杂度。而原地排序(如堆排序)虽然空间为 O(1)O(1),但常数因子更大且不稳定。

案例 2:搜索中的索引/哈希表

线性搜索 O(n)O(n) 无需额外空间;构建哈希索引后查找降至 O(1)O(1) 平均,但需要 O(n)O(n) 额外空间。这是典型的空间换时间。

案例 3:动态规划中的备忘录

递归解法(如斐波那契)时间 O(2n)O(2^n) 空间 O(n)O(n);加备忘录后时间 O(n)O(n) 空间 O(n)O(n);自底向上迭代后时间 O(n)O(n) 空间可优化至 O(1)O(1)

案例 4:布隆过滤器

mm 位比特数组近似表示 nn 个元素的集合,查询 O(k)O(k)kk 个哈希函数),空间仅 m/nm/n 比特/元素,代价是存在假阳性(误判存在)。

案例 5:LRU 缓存

LRU 缓存用 O(n)O(n) 额外空间(哈希表 + 双向链表)将缓存查询从 O(n)O(n)(线性扫描)降至 O(1)O(1),是空间换时间的经典案例。

4.3 权衡决策框架

是否需要最优时间? --是--> 空间是否充裕? --是--> 以空间换时间
  |                    |
  |                    +--否--> 寻找时间-空间折中方案
  |
  +--否--> 空间是否受限? --是--> 以时间换空间
           |
           +--否--> 平衡方案(如缓存策略)

量化分析:设 S(n)S(n) 为空间开销,T(n)T(n) 为时间开销。定义效率函数 E(n)=T(n)S(n)aE(n) = T(n) \cdot S(n)^a,其中 aa 为空间权重(0<a10 < a \leq 1)。选择使 E(n)E(n) 最小的方案。

4.4 工业级权衡决策表

场景时间优先空间优先推荐方案
数据库索引B+ 树 O(logn)O(\log n)哈希索引 O(1)O(1) 但不支持范围查询根据查询模式决定
Web 缓存LRU + 哈希表 O(1)O(1)不缓存 O(n)O(n)(DB 查询)LRU 缓存
字符串匹配后缀自动机 O(n)O(n) 预处理 + O(m)O(m) 查询KMP O(n+m)O(n+m)大文本多次查询用后缀自动机
图最短路Dijkstra O(ElogV)O(E \log V)Bellman-Ford O(VE)O(VE)非负权用 Dijkstra
大数据排序外部归并排序 O(nlogn)O(n \log n)原地堆排序 O(nlogn)O(n \log n)内存够用归并,受限用堆排

5. 递推与主定理

5.1 递归树方法

递归树是分析分治算法的直观工具。将递推式 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n) 展开为树形结构:

  • 根节点的工作量为 f(n)f(n)
  • 每个节点产生 aa 个子节点,每个子节点规模为 n/bn/b
  • ii 层有 aia^i 个节点,每个节点工作量为 f(n/bi)f(n/b^i)
  • 树的深度为 logbn\log_b n
  • 总工作量 = 各层工作量之和

可视化T(n)=2T(n/2)+cnT(n) = 2T(n/2) + cn 的递归树

                cn              第 0 层: cn
               /  \
             /    \
          cn/2    cn/2          第 1 层: cn
          / \     / \
        cn/4 cn/4 cn/4 cn/4     第 2 层: cn
        ... ... ... ...
        c  c  c  c  c  c        第 log n 层: cn
        总计: cn × (log n + 1) = O(n log n)

5.2 主定理(Master Theorem)

对于递推式 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n),其中 a1,b>1a \geq 1, b > 1

情形 1:若 f(n)=O(nlogbaϵ)f(n) = O(n^{\log_b a - \epsilon})(对某个 ϵ>0\epsilon > 0),则 T(n)=Θ(nlogba)T(n) = \Theta(n^{\log_b a})

直觉:叶子节点的总工作量支配根节点的工作量。

情形 2:若 f(n)=Θ(nlogbalogkn)f(n) = \Theta(n^{\log_b a} \log^k n)k0k \geq 0),则 T(n)=Θ(nlogbalogk+1n)T(n) = \Theta(n^{\log_b a} \log^{k+1} n)

特殊情况 k=0k = 0:若 f(n)=Θ(nlogba)f(n) = \Theta(n^{\log_b a}),则 T(n)=Θ(nlogbalogn)T(n) = \Theta(n^{\log_b a} \log n)

直觉:各层工作量大致相等。

情形 3:若 f(n)=Ω(nlogba+ϵ)f(n) = \Omega(n^{\log_b a + \epsilon})(对某个 ϵ>0\epsilon > 0),且正则条件 af(n/b)cf(n)a \cdot f(n/b) \leq c \cdot f(n)(对某个 c<1c < 1 和足够大的 nn),则 T(n)=Θ(f(n))T(n) = \Theta(f(n))

直觉:根节点的工作量支配叶子节点的总工作量。

5.3 主定理应用示例

递推式aabblogba\log_b af(n)f(n)情形
T(n)=2T(n/2)+nT(n) = 2T(n/2) + n221nn2O(nlogn)O(n \log n)
T(n)=2T(n/2)+1T(n) = 2T(n/2) + 1221111O(n)O(n)
T(n)=2T(n/2)+n2T(n) = 2T(n/2) + n^2221n2n^23O(n2)O(n^2)
T(n)=4T(n/2)+nT(n) = 4T(n/2) + n422nn1O(n2)O(n^2)
T(n)=4T(n/2)+n2T(n) = 4T(n/2) + n^2422n2n^22O(n2logn)O(n^2 \log n)
T(n)=3T(n/4)+nlognT(n) = 3T(n/4) + n \log n340.79nlognn \log n3O(nlogn)O(n \log n)
T(n)=8T(n/2)+n3T(n) = 8T(n/2) + n^3823n3n^32O(n3logn)O(n^3 \log n)
T(n)=7T(n/2)+n2T(n) = 7T(n/2) + n^2722.807n2n^21O(n2.807)O(n^{2.807})(Strassen)
T(n)=3T(n/2)+nT(n) = 3T(n/2) + n321.585nn1O(n1.585)O(n^{1.585})(Karatsuba)

5.4 主定理的证明思路

证明(情形 2,k=0k = 0,即 f(n)=Θ(nlogba)f(n) = \Theta(n^{\log_b a})):

f(n)=Θ(nlogba)f(n) = \Theta(n^{\log_b a}),即 c1,c2>0\exists c_1, c_2 > 0,对足够大 nnc1nlogbaf(n)c2nlogbac_1 n^{\log_b a} \leq f(n) \leq c_2 n^{\log_b a}

递归树第 ii 层(0ilogbn0 \leq i \leq \log_b n):

  • 节点数:aia^i
  • 每节点工作量:f(n/bi)=Θ((n/bi)logba)=Θ(nlogba/ai)f(n/b^i) = \Theta((n/b^i)^{\log_b a}) = \Theta(n^{\log_b a} / a^i)
  • ii 层总工作量:aiΘ(nlogba/ai)=Θ(nlogba)a^i \cdot \Theta(n^{\log_b a} / a^i) = \Theta(n^{\log_b a})

总工作量:

T(n)=i=0logbnΘ(nlogba)=Θ(nlogba)(logbn+1)=Θ(nlogbalogn)T(n) = \sum_{i=0}^{\log_b n} \Theta(n^{\log_b a}) = \Theta(n^{\log_b a}) \cdot (\log_b n + 1) = \Theta(n^{\log_b a} \log n)

\blacksquare

5.5 代入法

代入法(Substitution Method)是更通用的递推求解方法,步骤:

  1. 猜测:根据递推式猜测 T(n)=O(f(n))T(n) = O(f(n))
  2. 归纳:假设 T(k)cf(k)T(k) \leq c \cdot f(k) 对所有 k<nk < n 成立
  3. 验证:证明 T(n)cf(n)T(n) \leq c \cdot f(n)

示例T(n)=2T(n/2)+nT(n) = 2T(n/2) + n

猜测 T(n)=O(nlogn)T(n) = O(n \log n)。假设 T(k)cklogkT(k) \leq c \cdot k \log k 对所有 k<nk < n 成立。则

T(n)=2T(n/2)+n2c(n/2)log(n/2)+n=cn(logn1)+n=cnlogn+(1c)nT(n) = 2T(n/2) + n \leq 2c \cdot (n/2) \log(n/2) + n = c n (\log n - 1) + n = c n \log n + (1 - c) n

c1c \geq 1 时,T(n)cnlognT(n) \leq c n \log n\blacksquare

5.6 Akra-Bazzi 方法

Akra-Bazzi 1998 方法适用于更一般的递推形式:

T(x)=i=1kaiT(bix+hi(x))+g(x)T(x) = \sum_{i=1}^{k} a_i T(b_i x + h_i(x)) + g(x)

求解步骤:

  1. pp 满足 i=1kaibip=1\sum_{i=1}^{k} a_i b_i^p = 1
  2. 解为 T(x)=Θ(xp(1+1xg(u)up+1du))T(x) = \Theta\left(x^p \left(1 + \int_1^x \frac{g(u)}{u^{p+1}} du\right)\right)

示例T(n)=2T(n/2)+nlognT(n) = 2T(n/2) + \frac{n}{\log n}

p=1p = 1 满足 2(1/2)1=12 \cdot (1/2)^1 = 1。则

T(n)=Θ(n(1+1nu/loguu2du))=Θ(n(1+1nduulogu))=Θ(nloglogn)T(n) = \Theta\left(n \left(1 + \int_1^n \frac{u/\log u}{u^2} du\right)\right) = \Theta\left(n \left(1 + \int_1^n \frac{du}{u \log u}\right)\right) = \Theta(n \log \log n)

5.7 Python 实现:递归树可视化

import math
from typing import Callable

def analyze_recurrence(a: int, b: int, f_n: Callable[[float], float], n: int) -> tuple[list[float], float]:
    """
    分析递推式 T(n) = aT(n/b) + f(n) 的各层工作量。
    
    参数:
        a: 子问题数
        b: 子问题规模缩减因子
        f_n: 合并代价函数
        n: 输入规模
    
    返回:
        (各层工作量列表, 总工作量)
    """
    log_b_a = math.log(a, b)
    levels = int(math.log(n, b))
    work_per_level = []
    for i in range(levels + 1):
        num_nodes = a ** i
        work_per_node = f_n(n / (b ** i))
        level_work = num_nodes * work_per_node
        work_per_level.append(level_work)
    total = sum(work_per_level)
    return work_per_level, total

# 归并排序递推: T(n) = 2T(n/2) + n
def f_merge_sort(n: float) -> float:
    return n

work, total = analyze_recurrence(2, 2, f_merge_sort, 1024)
for i, w in enumerate(work):
    print(f"Level {i}: {w:.1f}")
print(f"Total: {total:.1f}")
# Level 0: 1024.0
# Level 1: 1024.0
# ...
# Level 10: 1024.0
# Total: 11264.0 ≈ 1024 * 11 = 1024 * (log2(1024) + 1)

5.8 C++ 实现:主定理判定

#include <iostream>
#include <vector>
#include <cmath>
#include <functional>
#include <string>
#include <iomanip>
using namespace std;

// 主定理判定器
struct MasterTheoremResult {
    int case_num;          // 情形 1/2/3
    string solution;       // 渐近解
    string explanation;    // 解释
};

MasterTheoremResult applyMasterTheorem(int a, int b, const string& f_n_desc, double log_b_a, double f_exponent) {
    // f_exponent: f(n) 的渐近指数(如 f(n)=n^2 -> 2; f(n)=n -> 1; f(n)=1 -> 0)
    // log_b_a: log_b(a)
    cout << fixed << setprecision(4);
    cout << "a=" << a << ", b=" << b << ", log_b(a)=" << log_b_a << ", f(n) ~ n^" << f_exponent << endl;
    
    if (f_exponent < log_b_a - 1e-9) {
        return {1, "Theta(n^" + to_string(log_b_a) + ")", "情形1: 叶子工作量支配"};
    } else if (abs(f_exponent - log_b_a) < 1e-9) {
        return {2, "Theta(n^" + to_string(log_b_a) + " * log n)", "情形2: 各层工作量相等"};
    } else if (f_exponent > log_b_a + 1e-9) {
        return {3, "Theta(f(n))", "情形3: 根节点工作量支配"};
    }
    return {0, "主定理不适用", "需用代入法或 Akra-Bazzi"};
}

int main() {
    // 归并排序: T(n) = 2T(n/2) + n
    auto r1 = applyMasterTheorem(2, 2, "n", log(2)/log(2), 1.0);
    cout << "归并排序: " << r1.solution << " (" << r1.explanation << ")" << endl;
    
    // Strassen: T(n) = 7T(n/2) + n^2
    auto r2 = applyMasterTheorem(7, 2, "n^2", log(7)/log(2), 2.0);
    cout << "Strassen: " << r2.solution << " (" << r2.explanation << ")" << endl;
    
    // Karatsuba: T(n) = 3T(n/2) + n
    auto r3 = applyMasterTheorem(3, 2, "n", log(3)/log(2), 1.0);
    cout << "Karatsuba: " << r3.solution << " (" << r3.explanation << ")" << endl;
    
    return 0;
}

5.9 Java 实现:复杂度计算器

import java.util.function.Function;

public class ComplexityAnalyzer {
    
    /**
     * 主定理判定器。
     * @param a 子问题数
     * @param b 子问题规模缩减因子
     * @param fExponent f(n) 的渐近指数
     * @return 渐近解字符串
     */
    public static String masterTheorem(int a, int b, double fExponent) {
        double logBA = Math.log(a) / Math.log(b);
        final double EPS = 1e-9;
        
        if (fExponent < logBA - EPS) {
            return String.format("情形1: Theta(n^%.4f) (叶子支配)", logBA);
        } else if (Math.abs(fExponent - logBA) < EPS) {
            return String.format("情形2: Theta(n^%.4f * log n) (各层相等)", logBA);
        } else {
            return String.format("情形3: Theta(f(n)) = Theta(n^%.4f) (根支配)", fExponent);
        }
    }
    
    /**
     * 递归树分析。
     */
    public static double[] recursionTree(int a, int b, Function<Double, Double> f, int n) {
        int levels = (int) (Math.log(n) / Math.log(b));
        double[] workPerLevel = new double[levels + 1];
        for (int i = 0; i <= levels; i++) {
            double numNodes = Math.pow(a, i);
            double workPerNode = f.apply(n / Math.pow(b, i));
            workPerLevel[i] = numNodes * workPerNode;
        }
        return workPerLevel;
    }
    
    public static void main(String[] args) {
        // 归并排序
        System.out.println("归并排序: " + masterTheorem(2, 2, 1.0));
        // Strassen
        System.out.println("Strassen: " + masterTheorem(7, 2, 2.0));
        // Karatsuba
        System.out.println("Karatsuba: " + masterTheorem(3, 2, 1.0));
        // 二分查找
        System.out.println("二分查找: " + masterTheorem(1, 2, 0.0));
    }
}

6. 摊还分析

6.1 为什么需要摊还分析

最坏情况分析有时过于悲观。某些数据结构的大部分操作代价很低,偶尔出现一次高代价操作。摊还分析(Amortized Analysis,Sleator-Tarjan 1985)关注 nn 个操作的序列总代价,而非单个操作的最坏代价,从而给出更紧的界。

关键区别:

  • 平均情况分析:依赖概率假设(如随机化快速排序的 O(nlogn)O(n \log n) 期望)
  • 摊还分析:不依赖概率,保证对任意操作序列成立

6.2 聚合分析(Aggregate Method)

nn 个操作的序列,计算总代价的上界 T(n)T(n),则每个操作的摊还代价为 T(n)/nT(n)/n

示例:动态数组扩容

动态数组(如 C++ vector、Python list、Java ArrayList)在容量满时扩容为原来的 2 倍:

class DynamicArray:
    """动态数组实现:容量满时倍增扩容。"""
    
    def __init__(self):
        self.capacity = 1
        self.size = 0
        self.data = [None] * self.capacity
    
    def push_back(self, val: int) -> None:
        """向数组末尾添加元素,均摊 O(1)。"""
        if self.size == self.capacity:
            # 扩容:旧数据复制到新数组
            new_data = [None] * (self.capacity * 2)
            for i in range(self.size):
                new_data[i] = self.data[i]
            self.data = new_data
            self.capacity *= 2
        self.data[self.size] = val
        self.size += 1

分析 nn 次 push_back 的总代价

  • 普通插入:O(1)O(1) 每次
  • 扩容发生在第 1,2,4,8,,2k1, 2, 4, 8, \ldots, 2^k 次插入时
  • 扩容代价:1+2+4++n/2<n1 + 2 + 4 + \ldots + n/2 < n
  • 总代价:nn(普通插入)+n+ n(扩容)=O(n)= O(n)
  • 摊还代价O(n)/n=O(1)O(n)/n = O(1)
#include <vector>
#include <iostream>
using namespace std;

class DynamicArray {
    int* data;
    int cap, sz;
public:
    DynamicArray() : data(new int[1]), cap(1), sz(0) {}
    ~DynamicArray() { delete[] data; }
    
    // 均摊 O(1) 的 push_back
    void push_back(int val) {
        if (sz == cap) {
            int* newData = new int[cap * 2];
            for (int i = 0; i < sz; i++) newData[i] = data[i];
            delete[] data;
            data = newData;
            cap *= 2;
        }
        data[sz++] = val;
    }
    
    int operator[](int i) const { return data[i]; }
    int size() const { return sz; }
    int capacity() const { return cap; }
};

int main() {
    DynamicArray arr;
    for (int i = 0; i < 16; i++) {
        arr.push_back(i);
        cout << "size=" << arr.size() << ", cap=" << arr.capacity() << endl;
    }
    return 0;
}

6.3 核算法(Accounting Method)

为不同类型的操作赋予不同的摊还代价(“收费”),使得:

  • 摊还代价 \geq 实际代价时,多余部分作为”信用”存储在数据结构中
  • 摊还代价 << 实际代价时,用存储的信用来支付差额
  • 信用始终非负

动态数组 push_back 的核算法分析

  • 普通插入:实际代价 1,摊还代价 3(1 用于插入,1 作为信用留给未来扩容时的复制,1 留给配对元素)
  • 扩容插入:实际代价 =1= 1(插入)+sz+ sz(复制),但 szsz 个元素的信用恰好支付复制代价
  • 摊还代价 =O(1)= O(1)

6.4 势能法(Potential Method)

Sleator-Tarjan 1985 系统化的势能法。定义势能函数 Φ(D)\Phi(D) 将数据结构状态映射为非负实数。操作 ii 的摊还代价:

c^i=ci+Φ(Di)Φ(Di1)\hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1})

其中 cic_i 为实际代价。若 Φ(D0)=0\Phi(D_0) = 0 且对所有 iiΦ(Di)0\Phi(D_i) \geq 0,则总摊还代价是总实际代价的上界。

动态数组的势能函数Φ(D)=2sizecapacity\Phi(D) = 2 \cdot \text{size} - \text{capacity}

  • 普通插入(未扩容):c^i=1+(2(sz+1)cap)(2szcap)=1+2=3\hat{c}_i = 1 + (2(sz+1) - cap) - (2 sz - cap) = 1 + 2 = 3
  • 扩容插入(capcapkk 变为 2k2kszszkk 变为 k+1k+1):

c^i=(1+k)+(2(k+1)2k)(2kk)=1+k+2k=3\hat{c}_i = (1 + k) + (2(k+1) - 2k) - (2k - k) = 1 + k + 2 - k = 3

两种情况下摊还代价均为 3,即 O(1)O(1)

6.5 多重弹栈示例

考虑一个栈支持三种操作:

  • Push(S, x)O(1)O(1)
  • Pop(S)O(1)O(1)
  • Multipop(S, k):弹出 min(k,S)\min(k, |S|) 个元素,O(min(k,S))O(\min(k, |S|))

nn 个操作序列的聚合分析

  • 每个元素最多被 push 一次、pop 一次
  • 总 pop 次数(包括 Multipop 中的)不超过总 push 次数
  • 总代价 2n=O(n)\leq 2n = O(n)
  • 摊还代价 =O(1)= O(1)

6.6 Splay 树的摊还分析

Sleator-Tarjan 1985 证明 Splay 树的所有操作(search/insert/delete)摊还 O(logn)O(\log n)。势能函数:

Φ(T)=xTlog2Tx\Phi(T) = \sum_{x \in T} \log_2 |T_x|

其中 Tx|T_x| 为以 xx 为根的子树大小,logTx\log |T_x| 称为节点 xx(rank)。

伸展操作(Splay)的摊还代价分析涉及 zig/zig-zig/zig-zag 三种情形,每种均能证明摊还代价 3(logTlogTx)+O(1)\leq 3(\log |T| - \log |T_x|) + O(1),求和后摊还 O(logn)O(\log n)

6.7 三种方法对比

方法核心思想适用场景难点
聚合分析nn 个操作总代价操作序列结构简单复杂序列难精确求和
核算法为操作赋予”摊还代价” + 信用操作类型明确信用分配需巧妙设计
势能法定义势能函数 Φ(D)\Phi(D)数据结构状态明确势能函数设计需要洞察力

7. 算法设计范式总览

7.1 五大范式对比

范式核心思想适用条件典型问题时间复杂度特征
分治分解-解决-合并子问题独立归并排序、快速排序O(nlogn)O(n \log n)
贪心每步取局部最优贪心选择性质+最优子结构活动选择、Huffman 编码通常 O(nlogn)O(n \log n)
动态规划记忆化避免重复最优子结构+无后效性背包、LCS、编辑距离依赖状态空间大小
回溯系统搜索+剪枝解空间可枚举N 皇后、子集生成指数级(最坏)
分支限界BFS 搜索+下界剪枝可计算下界TSP、整数规划指数级(最坏)

7.2 分治 vs 贪心 vs 动态规划

分治(Divide and Conquer)的本质是”独立”:

  • 将问题分解为互不重叠的子问题
  • 分别求解后合并
  • 子问题之间没有依赖关系
  • 例子:归并排序中左半和右半独立排序

贪心(Greedy)的本质是”短视”:

  • 在每一步做出局部最优选择
  • 不回头、不撤销
  • 依赖贪心选择性质:局部最优能导向全局最优
  • 例子:Dijkstra 算法中每次选最近节点

动态规划(Dynamic Programming)的本质是”记忆”:

  • 子问题重叠,需要避免重复计算
  • 通过记录子问题的解来消除冗余
  • 依赖最优子结构:最优解包含子问题的最优解
  • 例子:Floyd-Warshall 中 dist[k][i][j]\text{dist}[k][i][j] 依赖 dist[k1]\text{dist}[k-1]

决策流程

问题是否可分解为独立子问题? --是--> 分治
  |
  +--否--> 问题是否有最优子结构? --否--> 回溯/暴力搜索
           |
           +--是--> 局部最优能否导向全局最优? --是--> 贪心
                    |
                    +--否--> 子问题是否重叠? --是--> 动态规划
                             |
                             +--否--> 分治(但可能需要更巧妙的分解)

7.3 范式选择的常见误区

  1. 贪心与 DP 混淆:0-1 背包贪心不可行,但分数背包贪心可行。关键区别在于物品是否可分割——不可分割导致贪心选择的”不可逆”与全局最优矛盾。
  2. 分治与 DP 混淆:分治的子问题不重叠,DP 的子问题重叠。如果分治递归中出现大量重复子问题,应考虑 DP。
  3. 回溯与分支限界混淆:回溯用 DFS 搜索,分支限界用 BFS/最佳优先。回溯适合找一个解,分支限界适合找最优解。

跨模块引用:各范式的详细实现分别参见 排序算法(分治)、贪心算法动态规划递归与回溯


8. 经典应用案例

8.1 案例一:归并排序复杂度分析

算法(von Neumann 1945):

def merge_sort(arr: list[int]) -> list[int]:
    """归并排序:T(n) = 2T(n/2) + O(n) = O(n log n)。"""
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

def merge(a: list[int], b: list[int]) -> list[int]:
    """合并两个有序数组 O(n)。"""
    result = []
    i = j = 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:
            result.append(a[i])
            i += 1
        else:
            result.append(b[j])
            j += 1
    result.extend(a[i:])
    result.extend(b[j:])
    return result

复杂度分析

递推关系:T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)

由主定理情形 2(a=2,b=2,logba=1,f(n)=n=Θ(n1)a = 2, b = 2, \log_b a = 1, f(n) = n = \Theta(n^1)):

T(n)=Θ(nlogn)T(n) = \Theta(n \log n)

空间复杂度:O(n)O(n)(辅助数组)

8.2 案例二:快速排序期望分析

算法(Hoare 1961):

import random

def quick_sort(arr: list[int]) -> list[int]:
    """随机化快速排序:期望 O(n log n),最坏 O(n^2)。"""
    if len(arr) <= 1:
        return arr
    pivot_idx = random.randint(0, len(arr) - 1)
    pivot = arr[pivot_idx]
    left = [x for i, x in enumerate(arr) if x <= pivot and i != pivot_idx]
    right = [x for i, x in enumerate(arr) if x > pivot and i != pivot_idx]
    return quick_sort(left) + [pivot] + quick_sort(right)

期望复杂度分析

T(n)T(n) 为期望时间。随机 pivot 等概率选取,子问题规模 kkn1kn - 1 - k0kn10 \leq k \leq n-1):

T(n)=1nk=0n1[T(k)+T(n1k)]+O(n)=2nk=0n1T(k)+O(n)T(n) = \frac{1}{n} \sum_{k=0}^{n-1} [T(k) + T(n-1-k)] + O(n) = \frac{2}{n} \sum_{k=0}^{n-1} T(k) + O(n)

用代入法证明 T(n)=O(nlogn)T(n) = O(n \log n):假设 T(k)cklogkT(k) \leq c k \log k 对所有 k<nk < n 成立。则

T(n)2cnk=1n1klogk+O(n)2cnn2logn2+O(n)=cnlogn+O(n)T(n) \leq \frac{2c}{n} \sum_{k=1}^{n-1} k \log k + O(n) \leq \frac{2c}{n} \cdot \frac{n^2 \log n}{2} + O(n) = c n \log n + O(n)

(用积分 1nxlogxdxn2logn2\int_1^n x \log x \, dx \leq \frac{n^2 \log n}{2}

T(n)=O(nlogn)T(n) = O(n \log n)\blacksquare

8.3 案例三:二分查找

算法

def binary_search(arr: list[int], target: int) -> int:
    """二分查找:T(n) = T(n/2) + O(1) = O(log n)。"""
    lo, hi = 0, len(arr) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1

复杂度分析

递推关系:T(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)

由主定理情形 1(a=1,b=2,logba=0,f(n)=1=O(n0ϵ)a = 1, b = 2, \log_b a = 0, f(n) = 1 = O(n^{0 - \epsilon}) 对任意 ϵ>0\epsilon > 0):

T(n)=Θ(n0)=Θ(1)Θ(logn)=O(logn)T(n) = \Theta(n^0) = \Theta(1) \cdot \Theta(\log n) = O(\log n)

8.4 案例四:Karatsuba 大整数乘法

算法(Karatsuba 1960):将 nn 位整数 x=x110n/2+x0x = x_1 \cdot 10^{n/2} + x_0, y=y110n/2+y0y = y_1 \cdot 10^{n/2} + y_0 的乘法分解为 3 次递归乘法:

z0=x0y0,z2=x1y1,z1=(x0+x1)(y0+y1)z0z2z_0 = x_0 y_0, \quad z_2 = x_1 y_1, \quad z_1 = (x_0 + x_1)(y_0 + y_1) - z_0 - z_2

xy=z210n+z110n/2+z0xy = z_2 \cdot 10^n + z_1 \cdot 10^{n/2} + z_0

def karatsuba(x: int, y: int) -> int:
    """Karatsuba 大整数乘法:T(n) = 3T(n/2) + O(n) = O(n^1.585)。"""
    if x < 10 or y < 10:
        return x * y
    n = max(len(str(x)), len(str(y)))
    m = n // 2
    x1, x0 = divmod(x, 10 ** m)
    y1, y0 = divmod(y, 10 ** m)
    z0 = karatsuba(x0, y0)
    z2 = karatsuba(x1, y1)
    z1 = karatsuba(x0 + x1, y0 + y1) - z0 - z2
    return z2 * 10 ** (2 * m) + z1 * 10 ** m + z0

复杂度分析

递推:T(n)=3T(n/2)+O(n)T(n) = 3T(n/2) + O(n)

由主定理情形 1(a=3,b=2,logba=log231.585,f(n)=n=O(n1.585ϵ)a = 3, b = 2, \log_b a = \log_2 3 \approx 1.585, f(n) = n = O(n^{1.585 - \epsilon})ϵ0.585\epsilon \approx 0.585):

T(n)=Θ(nlog23)Θ(n1.585)T(n) = \Theta(n^{\log_2 3}) \approx \Theta(n^{1.585})

8.5 案例五:Strassen 矩阵乘法

算法(Strassen 1969):将 2×22 \times 2 矩阵乘法的 8 次乘法降至 7 次,递推 T(n)=7T(n/2)+O(n2)T(n) = 7T(n/2) + O(n^2)

由主定理情形 1(a=7,b=2,logba=log272.807,f(n)=n2=O(n2.807ϵ)a = 7, b = 2, \log_b a = \log_2 7 \approx 2.807, f(n) = n^2 = O(n^{2.807 - \epsilon})):

T(n)=Θ(nlog27)Θ(n2.807)T(n) = \Theta(n^{\log_2 7}) \approx \Theta(n^{2.807})

首次突破朴素 O(n3)O(n^3)

8.6 案例六:动态数组扩容均摊

参见第 6.2 节。

8.7 案例七:Splay 树

Sleator-Tarjan 1985 的 Splay 树,每次操作摊还 O(logn)O(\log n)。势能函数 Φ(T)=xTlogTx\Phi(T) = \sum_{x \in T} \log |T_x|。详见 章节。


9. 工程实践

9.1 实战优化技巧

  1. 常数优化:渐近复杂度相同不代表实际性能相同。例如插入排序对小数组(n<16n < 16)通常比归并排序快,因常数因子小。C++ std::sort 使用 introsort(快排 + 堆排 + 插入排序)混合策略。
  2. 缓存友好性:顺序访问比随机访问快 10-100 倍(L1/L2/L3 缓存)。归并排序的顺序合并比快排的随机 pivot 访问更缓存友好。
  3. 分支预测:现代 CPU 分支预测正确率 > 95%,但错误预测代价 10-20 周期。排序后用二分查找比线性扫描+条件判断更快。
  4. SIMD 向量化:现代 CPU 支持 AVX2/AVX-512,可并行处理 8/16 个 32 位整数。NumPy 的 np.sort 利用 SIMD 加速。
  5. 内存对齐:结构体字段对齐可显著提升缓存命中率。__attribute__((aligned(64))) 强制 64 字节对齐(缓存行大小)。
  6. 预分配:动态数组预分配可避免多次扩容。Python list(range(n))[i for i in range(n)] 快约 2 倍。
  7. 惰性计算:仅在实际需要时计算,避免无用工作。Python 生成器 yield 实现惰性序列。
  8. 批处理:合并多次小操作为一次大操作。如 list.extend 比多次 list.append 快。

9.2 复杂度分析工具

import time
import random
from functools import wraps

def measure_time(func):
    """测量函数运行时间。"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__}: {elapsed:.6f}s")
        return result
    return wrapper

def empirical_complexity(func, n_values: list[int], repeat: int = 3):
    """
    经验复杂度估计:通过多次运行估计算法复杂度。
    
    参数:
        func: 接受规模 n 返回输入数据的函数
        n_values: 测试规模列表
        repeat: 每个规模重复次数(取中位数)
    
    返回:
        {n: median_time}
    """
    import statistics
    results = {}
    for n in n_values:
        times = []
        for _ in range(repeat):
            data = func(n)
            start = time.perf_counter()
            # 在此处调用待测算法
            _ = sorted(data)  # 示例
            times.append(time.perf_counter() - start)
        results[n] = statistics.median(times)
    return results

# 估计 sorted() 的复杂度
import math
n_values = [1000, 2000, 4000, 8000, 16000]
gen = lambda n: [random.randint(0, n) for _ in range(n)]
results = empirical_complexity(gen, n_values)

# 计算相邻规模的时间比,判断复杂度
for i in range(1, len(n_values)):
    ratio = results[n_values[i]] / results[n_values[i-1]]
    print(f"n={n_values[i]}: {results[n_values[i]]:.6f}s, ratio={ratio:.2f}")
# O(n log n) 时,n 翻倍时间应约 2.1 倍

9.3 基准测试框架

import timeit
import random
import statistics

def benchmark(sort_func, sizes=(100, 1000, 10000, 100000), repeat=5):
    """基准测试框架。"""
    results = {}
    for n in sizes:
        times = []
        for _ in range(repeat):
            data = [random.randint(0, n) for _ in range(n)]
            timer = timeit.Timer(lambda: sort_func(data.copy()))
            times.append(timer.timeit(number=1))
        results[n] = {
            'min': min(times),
            'median': statistics.median(times),
            'mean': statistics.mean(times),
            'max': max(times),
        }
    return results

# 测试 Python 内置 sort
print(benchmark(sorted))

9.4 工业级优化案例

案例 1:NumPy 排序优化

NumPy 提供多种排序算法,根据数据特征选择:

import numpy as np

arr = np.random.randint(0, 1000000, size=1000000)
# quicksort:默认,平均 O(n log n),最坏 O(n^2)
np.sort(arr, kind='quicksort')
# mergesort:稳定,O(n log n),需 O(n) 额外空间
np.sort(arr, kind='mergesort')
# heapsort:原地,O(n log n),不稳定
np.sort(arr, kind='heapsort')
# timsort:Python list 默认,混合归并+插入
sorted(arr.tolist())

案例 2:Linux 内核排序

Linux 内核的 lib/sort.c 提供堆排序与归并排序两种实现:

  • sort():堆排序,原地,不稳定,O(nlogn)O(n \log n)
  • 对小数组(n<16n < 16)切换至插入排序

案例 3:PostgreSQL 外部排序

PostgreSQL 使用多路归并排序处理大于内存的数据集:

  1. 内部排序生成有序归并段(run)
  2. 多路归并段合并为更大的归并段
  3. 重复直至所有数据合并为一个有序文件

复杂度 O(nlogn)O(n \log n),I/O 复杂度 O(nlogM/B(n/B))O(n \log_{M/B} (n/B))MM 为内存,BB 为块大小)。

9.5 性能反模式

  1. 过早优化:Knuth 1974 Turing Award 演讲《Computer Programming as an Art》:“Premature optimization is the root of all evil”(过早优化是万恶之源)。
  2. 忽视常数O(n)O(n) 不一定比 O(nlogn)O(n \log n) 快,当常数因子相差 100 倍时。
  3. 错误的最坏情况:快排的 O(n2)O(n^2) 最坏情况在已排序数据上确实发生。
  4. 忽视缓存O(n)O(n) 的随机访问可能比 O(nlogn)O(n \log n) 的顺序访问慢。
  5. 过度抽象:模板元编程/泛型过度可能掩盖实际复杂度。

10. 常见陷阱

10.1 陷阱一:混淆 OOΘ\Theta

错误

快排是 O(n log n) 算法

正确

快排的平均复杂度是 Theta(n log n),最坏是 Theta(n^2)。
"O(n log n)" 严格说是上界,"Theta(n log n)" 才是紧界。

10.2 陷阱二:忽视编码方式

错误:称 0-1 背包 DP O(nW)O(nW) 是多项式时间。

正确O(nW)O(nW)伪多项式时间。WW 在二进制编码下为 logW\log W 位,故 W=2logWW = 2^{\log W},实际复杂度为 O(n2logW)O(n \cdot 2^{\log W}),是指数级。

10.3 陷阱三:忽视常数因子

错误:认为 O(n)O(n) 算法永远比 O(nlogn)O(n \log n) 算法快。

正确:当 nn 较小或 O(n)O(n) 算法常数因子大时,O(nlogn)O(n \log n) 可能更快。例如基数排序 O(dn)O(dn)dd 较大时不如快排 O(nlogn)O(n \log n)

10.4 陷阱四:忽视最坏情况

错误:朴素快排对所有数据都快。

正确:朴素快排在已排序数据上为 O(n2)O(n^2)。需随机化 pivot 或使用 introsort。

10.5 陷阱五:错误使用主定理

错误T(n)=2T(n/2)+n/lognT(n) = 2T(n/2) + n/\log n 用主定理情形 2 得 O(nlogn)O(n \log n)

正确f(n)=n/lognf(n) = n/\log n 不满足主定理任一情形的严格条件(logkn\log^k nk=1k = -1 不满足 k0k \geq 0)。需用 Akra-Bazzi 方法,得 T(n)=Θ(nloglogn)T(n) = \Theta(n \log \log n)

10.6 陷阱六:混淆平均与摊还

错误:“动态数组 push_back 平均 O(1)O(1)”。

正确:动态数组 push_back 摊还 O(1)O(1),不依赖概率假设;“平均”暗含概率分布。

10.7 陷阱七:递归深度爆炸

错误:Python 默认递归深度 1000,计算 fib(2000) 直接报 RecursionError

正确:使用 sys.setrecursionlimit(10000) 或改为迭代。

import sys
sys.setrecursionlimit(10000)

# 或改为迭代
def fib_iter(n: int) -> int:
    if n < 2:
        return n
    a, b = 0, 1
    for _ in range(n - 1):
        a, b = b, a + b
    return b

10.8 陷阱八:浮点数比较

错误if abs(a - b) < 1e-9: 用固定阈值比较浮点数。

正确:用相对误差 if abs(a - b) < 1e-9 * max(abs(a), abs(b)):

10.9 陷阱九:忽视栈溢出

错误:归并排序递归实现处理 10610^6 元素时栈溢出。

正确:改为自底向上迭代实现。

def merge_sort_iterative(arr: list[int]) -> list[int]:
    """自底向上归并排序,避免递归栈溢出。"""
    n = len(arr)
    width = 1
    while width < n:
        for i in range(0, n, 2 * width):
            mid = min(i + width, n)
            end = min(i + 2 * width, n)
            arr[i:end] = merge(arr[i:mid], arr[mid:end])
        width *= 2
    return arr

10.10 陷阱十:错误估计空间复杂度

错误:归并排序空间 O(logn)O(\log n)(仅算递归栈)。

正确:归并排序空间 O(n)O(n)(递归栈 O(logn)O(\log n) + 辅助数组 O(n)O(n))。


11. 学习路线图

11.1 三阶段学习路径

第一阶段:基础(4-6 周)

目标:掌握基本数据结构与经典算法,能独立完成 Easy 难度题目。

周次主题核心内容练习量
1-2数据结构基础数组、链表、栈、队列15 题
3-4排序与搜索排序算法、二分搜索、哈希表15 题
5-6树与递归二叉树遍历、递归思维15 题

第二阶段:进阶(6-8 周)

目标:掌握算法设计范式,能独立完成 Medium 难度题目。

周次主题核心内容练习量
7-9动态规划背包、LCS、区间 DP25 题
10-11图算法BFS/DFS、最短路、拓扑排序20 题
12-14贪心与回溯贪心证明、回溯剪枝20 题

第三阶段:专题(持续)

目标:深入特定方向,攻克 Hard 题目。

方向核心内容参考资源
字符串算法KMP、后缀数组、AC 自动机算法竞赛进阶指南
计算几何凸包、线段交、半平面交计算几何算法与应用
高级数据结构线段树、树状数组、LCT国家集训队论文
数学与数论快速幂、GCD、素数筛算法竞赛入门经典

11.2 学习节奏建议

  • 每日投入 1.5-2 小时
  • 每日 2-3 题:1 题复习 + 1 题新题 + 可选 1 题挑战
  • 每周 1 次总结:回顾本周题目,归纳模式
  • 每 2 周 1 次模拟:限时完成 3-4 题

跨模块引用:具体刷题策略参见 LeetCode 刷题指南

11.3 推荐资源

经典教材

  1. CLRS《Introduction to Algorithms》4th ed (2022):MIT 6.006/6.046 教材,全球最权威
  2. Sedgewick-Wayne《Algorithms》4th ed (2011):Princeton COS 226 教材,图示丰富
  3. Kleinberg-Tardos《Algorithm Design》 (2006):Cornell CS 4820 教材,设计范式深入
  4. Skiena《The Algorithm Design Manual》3rd ed (2020):Stony Brook CSE 373 教材,实战导向
  5. Knuth《TAOCP》Vol.1-4:算法分析的开山与权威

在线课程

  1. MIT 6.006 Introduction to Algorithms (OCW 免费)
  2. Stanford CS 161 Design and Analysis of Algorithms
  3. Princeton COS 226 Data Structures and Algorithms (Coursera)
  4. UC Berkeley CS 170 Efficient Algorithms and Intractable Problems
  5. CMU 15-451/651 Algorithm Design and Analysis

在线平台

  1. LeetCode:3000+ 题目,企业面试题库
  2. Codeforces:竞赛算法平台,定期 contest
  3. AtCoder:日本竞赛平台,题目质量高
  4. HackerRank:技能认证 + 算法题
  5. LintCode:中文 LeetCode 镜像

11.4 算法学习路径建议

graph TD
    A[算法分析基础] --> B[数据结构基础]
    B --> C[排序与搜索]
    C --> D[树与递归]
    D --> E[图算法]
    D --> F[动态规划]
    D --> G[贪心算法]
    D --> H[回溯与分支限界]
    E --> I[高级图算法]
    F --> J[高级 DP]
    G --> K[高级贪心]
    H --> L[高级搜索]
    I --> M[字符串算法]
    J --> M
    K --> N[计算几何]
    L --> O[高级数据结构]
    M --> P[算法竞赛]
    N --> P
    O --> P

12. 算法速查表

12.1 复杂度速查

数据结构访问搜索插入删除空间
数组O(1)O(1)O(n)O(n)O(n)O(n)O(n)O(n)O(n)O(n)
链表O(n)O(n)O(n)O(n)O(1)O(1)O(1)O(1)O(n)O(n)
O(n)O(n)O(n)O(n)O(1)O(1)O(1)O(1)O(n)O(n)
队列O(n)O(n)O(n)O(n)O(1)O(1)O(1)O(1)O(n)O(n)
哈希表N/AO(1)O(1)^*O(1)O(1)^*O(1)O(1)^*O(n)O(n)
BSTO(logn)O(\log n)^*O(logn)O(\log n)^*O(logn)O(\log n)^*O(logn)O(\log n)^*O(n)O(n)
红黑树O(logn)O(\log n)O(logn)O(\log n)O(logn)O(\log n)O(logn)O(\log n)O(n)O(n)
B 树O(logn)O(\log n)O(logn)O(\log n)O(logn)O(\log n)O(logn)O(\log n)O(n)O(n)
跳跃表O(logn)O(\log n)^*O(logn)O(\log n)^*O(logn)O(\log n)^*O(logn)O(\log n)^*O(n)O(n)
O(1)O(1)O(n)O(n)O(logn)O(\log n)O(logn)O(\log n)O(n)O(n)

* 平均复杂度,最坏情况可能退化

12.2 排序算法速查

算法最好平均最坏空间稳定原地
冒泡排序O(n)O(n)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)
选择排序O(n2)O(n^2)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)
插入排序O(n)O(n)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)
快速排序O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n2)O(n^2)O(logn)O(\log n)
归并排序O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n)O(n)
堆排序O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(1)O(1)
计数排序O(n+k)O(n+k)O(n+k)O(n+k)O(n+k)O(n+k)O(k)O(k)
基数排序O(dn)O(dn)O(dn)O(dn)O(dn)O(dn)O(n+d)O(n+d)
桶排序O(n+k)O(n+k)O(n+k)O(n+k)O(n2)O(n^2)O(n+k)O(n+k)
TimsortO(n)O(n)O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n)O(n)

12.3 图算法速查

算法时间复杂度空间复杂度适用场景
BFSO(V+E)O(V+E)O(V)O(V)无权最短路、层序遍历
DFSO(V+E)O(V+E)O(V)O(V)环检测、拓扑排序
DijkstraO(ElogV)O(E \log V)O(V)O(V)非负权最短路
Bellman-FordO(VE)O(VE)O(V)O(V)含负权最短路
Floyd-WarshallO(V3)O(V^3)O(V2)O(V^2)全源最短路
KruskalO(ElogE)O(E \log E)O(E)O(E)最小生成树(稀疏图)
PrimO(ElogV)O(E \log V)O(V)O(V)最小生成树(稠密图)
拓扑排序O(V+E)O(V+E)O(V)O(V)DAG 排序
Tarjan SCCO(V+E)O(V+E)O(V)O(V)强连通分量
网络流(Dinic)O(V2E)O(V^2 E)O(V+E)O(V+E)最大流

12.4 DP 经典问题速查

问题状态定义转移方程时间空间
斐波那契dp[i]: 第 iidp[i] = dp[i-1] + dp[i-2]O(n)O(n)O(1)O(1)
0-1 背包dp[i][w]: 前 ii 个容量 wwdp[i][w] = max(dp[i-1][w], dp[i-1][w-wi] + vi)O(nW)O(nW)O(W)O(W)
LCSdp[i][j]: 前 iijjdp[i][j] = max(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]+1)O(mn)O(mn)O(mn)O(mn)
LISdp[i]: 以 ii 结尾dp[i] = max(dp[j]+1) for j<i, a[j]<a[i]O(nlogn)O(n \log n)O(n)O(n)
编辑距离dp[i][j]: 前 iijjdp[i][j] = min(dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+cost)O(mn)O(mn)O(mn)O(mn)
矩阵链dp[i][j]: 链 iji \to jdp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j])O(n3)O(n^3)O(n2)O(n^2)

12.5 递推式求解速查

递推式典型算法
T(n)=T(n1)+O(1)T(n) = T(n-1) + O(1)O(n)O(n)线性扫描
T(n)=T(n1)+O(n)T(n) = T(n-1) + O(n)O(n2)O(n^2)选择排序
T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)O(nlogn)O(n \log n)归并排序
T(n)=2T(n/2)+O(1)T(n) = 2T(n/2) + O(1)O(n)O(n)数组求和(分治)
T(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)O(logn)O(\log n)二分搜索
T(n)=T(n/2)+O(n)T(n) = T(n/2) + O(n)O(n)O(n)快速选择
T(n)=3T(n/2)+O(n)T(n) = 3T(n/2) + O(n)O(n1.585)O(n^{1.585})Karatsuba 乘法
T(n)=7T(n/2)+O(n2)T(n) = 7T(n/2) + O(n^2)O(n2.807)O(n^{2.807})Strassen 矩阵
T(n)=T(n1)+T(n2)+O(1)T(n) = T(n-1) + T(n-2) + O(1)O(2n)O(2^n)朴素斐波那契
T(n)=2T(n1)+O(1)T(n) = 2T(n-1) + O(1)O(2n)O(2^n)汉诺塔
T(n)=nT(n1)T(n) = n \cdot T(n-1)O(n!)O(n!)全排列

13. 习题与参考答案

13.1 选择题

题 1(easy):下列哪个不是 O(n2)O(n^2) 的?

A. i=1ni\sum_{i=1}^{n} i B. i=1ni2\sum_{i=1}^{n} i^2 C. i=1n2i\sum_{i=1}^{n} 2^i D. nlogn+n2/2n \log n + n^2 / 2

答案

C。i=1n2i=2n+12=O(2n)\sum_{i=1}^{n} 2^i = 2^{n+1} - 2 = O(2^n),是指数级。

题 2(medium):递推 T(n)=4T(n/2)+n2lognT(n) = 4T(n/2) + n^2 \log n 的解是?

A. O(n2)O(n^2) B. O(n2logn)O(n^2 \log n) C. O(n2log2n)O(n^2 \log^2 n) D. O(n3)O(n^3)

答案

C。由主定理情形 2(a=4,b=2,logba=2,f(n)=n2logn=Θ(n2log1n)a = 4, b = 2, \log_b a = 2, f(n) = n^2 \log n = \Theta(n^2 \log^1 n)k=1k = 1),解为 Θ(n2logk+1n)=Θ(n2log2n)\Theta(n^2 \log^{k+1} n) = \Theta(n^2 \log^2 n)

题 3(medium):动态数组 push_back 的摊还复杂度是?

A. O(1)O(1) 最坏 B. O(1)O(1) 摊还 C. O(logn)O(\log n) 摊还 D. O(n)O(n) 摊还

答案

B。单次 push_back 最坏 O(n)O(n)(扩容时),但 nn 次操作总代价 O(n)O(n),摊还 O(1)O(1)

题 4(hard):递推 T(n)=2T(n/2)+n/lognT(n) = 2T(n/2) + n / \log n 的解是?

A. O(n)O(n) B. O(nlogn)O(n \log n) C. O(nloglogn)O(n \log \log n) D. 主定理不适用

答案

C。主定理不直接适用(f(n)=n/lognf(n) = n/\log n 不满足 nlogbalogknn^{\log_b a} \log^k nk0k \geq 0)。用 Akra-Bazzi:p=1p = 1T(n)=Θ(n(1+1nu/loguu2du))=Θ(n(1+loglogn))=Θ(nloglogn)T(n) = \Theta(n (1 + \int_1^n \frac{u/\log u}{u^2} du)) = \Theta(n (1 + \log \log n)) = \Theta(n \log \log n)

题 5(hard):下列陈述哪个正确?

A. P = NP 已被证明 B. NP 完全问题至少有一个被证明有多项式时间算法 C. SAT 是 NP 完全的(Cook-Levin 定理) D. 所有 NP 问题都是 NP 完全的

答案

C。Cook 1971 STOC 证明 SAT 是 NP 完全的(Cook-Levin 定理)。P vs NP 仍为开放问题。

13.2 填空题

题 1(easy):大 O 符号由德国数论学家 Paul ______ 在 1894 年的著作《Analytische Zahlentheorie》中首次引入,后由 Edmund ______ 推广。

答案

Bachmann, Landau

题 2(medium):主定理的递推形式为 T(n)=T(n) = ______,其中 aa \geq ______, b>b > ______。

答案

aT(n/b)+f(n)aT(n/b) + f(n), 1, 1

题 3(medium):摊还分析的三种方法是 、______。

答案

聚合分析(Aggregate Method)、核算方法(Accounting Method)、势能方法(Potential Method)

题 4(hard):T(n)=7T(n/2)+n2T(n) = 7T(n/2) + n^2 的解是 Θ(nlog27)Θ(\Theta(n^{\log_2 7}) \approx \Theta(______)),对应 ______ 算法。

答案

n2.807n^{2.807}, Strassen 矩阵乘法

13.3 代码修正题

题 1(medium):以下 Python 代码用于递归计算斐波那契数,但效率极低。请优化。

def fib(n: int) -> int:
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)
答案

朴素递归 O(2n)O(2^n),加记忆化后 O(n)O(n)

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n: int) -> int:
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

# 或自底向上迭代 O(n) 时间 O(1) 空间
def fib_iter(n: int) -> int:
    if n < 2:
        return n
    a, b = 0, 1
    for _ in range(n - 1):
        a, b = b, a + b
    return b

题 2(hard):以下代码在 n=105n = 10^5 时栈溢出,请修正。

def sum_range(arr: list[int], lo: int, hi: int) -> int:
    if lo > hi:
        return 0
    return arr[lo] + sum_range(arr, lo + 1, hi)

total = sum_range(list(range(10**5)), 0, 10**5 - 1)
答案

Python 默认递归深度 1000,需改为迭代:

def sum_range_iterative(arr: list[int]) -> int:
    total = 0
    for x in arr:
        total += x
    return total

total = sum_range_iterative(list(range(10**5)))
# 或直接使用内置 sum()
total = sum(range(10**5))

13.4 开放性论述题

题 1(medium):论述 P vs NP 问题的意义,并解释为何它被列为千禧年七大难题之首。

参考答案

P vs NP 问题的核心是:能否在多项式时间内验证解的问题,也能在多项式时间内求解?若 P = NP,则所有 NP 问题(包括 SAT、TSP 判定、子集和、图着色等 21 个 Karp 1972 NP 完全问题)都有多项式时间算法。这将产生深远影响:

  1. 密码学崩塌:RSA、ECC 等基于”大数分解是难解”的公钥密码体系失效(因分解 ∈ NP)
  2. 优化问题易解:TSP、背包、调度等组合优化问题都能高效求解
  3. AI 加速:自动定理证明、机器学习训练等任务大幅加速
  4. 数学自动化:定理证明可自动化(因定理证明 ∈ NP)

被列为千禧年七大难题之首的原因:

  1. 基础性:涉及”计算的本质”,是计算机科学最深层的开放问题
  2. 影响广泛:解决将颠覆密码学、优化、AI 等多个领域
  3. 难度极高:自 Cook 1971 提出至今 50+ 年无重大突破
  4. 理论意义:将揭示计算复杂性的本质结构

Clay 数学研究所 2000 年悬赏 100 万美元,至今无人领取。

题 2(hard):对比分析聚合分析、核算方法、势能方法三种摊还分析的优缺点,并各举一个应用实例。

参考答案
方法优点缺点应用实例
聚合分析直观易懂,无需设计势能函数仅适用于操作序列结构简单的情况;难以分析复杂操作序列多重弹栈:nn 个 Push/Pop/Multipop 总代价 O(n)O(n)
核算方法直观赋予”摊还代价”,易理解信用分配需巧妙设计,难以严格证明动态数组 push_back:摊还代价 3 = 1(插入)+ 1(自身复制信用)+ 1(配对元素复制信用)
势能方法最通用,适用于复杂数据结构势能函数设计需要洞察力,证明繁琐Splay 树:$\Phi(T) = \sum_x \log

综合对比

  • 聚合分析适合教学和简单数据结构,但对 Splay 树等复杂结构难以应用
  • 核算方法是聚合与势能的中间桥梁,但信用设计无系统方法
  • 势能方法最强大,可分析所有可摊还分析的问题,是 Sleator-Tarjan 1985 的核心贡献

工业实践中,势能方法用于分析 Redis 的 ziplist、Linux 内核的 LRU 缓存、PostgreSQL 的 WAL 日志等。

题 3(hard):论述”为什么多项式时间被作为’易解’(tractable)的判据”。这一选择有哪些哲学与实际考虑?有哪些反例?

参考答案

Cobham-Edmonds 论题:P 类(多项式时间)= “易解”问题。这一选择基于以下考虑:

支持理由

  1. 稳定性:多项式时间在所有合理的计算模型(图灵机、RAM、PRAM)下等价(Church-Turing 论题的扩展)
  2. 闭包性:多项式函数对加法、乘法、复合封闭,便于分析组合算法
  3. 理论与实践吻合:绝大多数实际高效算法是多项式时间,绝大多数”难解”问题(如 TSP、SAT)非多项式时间
  4. 渐近意义:任意多项式 nkn^kkk 任意大常数)渐近优于任意指数 2n2^n
  5. 资源可扩展性:多项式时间算法可通过硬件加速处理更大规模输入

反例与批评

  1. 大常数多项式O(n100)O(n^{100}) 理论上”易解”,但实际不可计算
  2. 小指数指数O(1.0001n)O(1.0001^n) 理论上”难解”,但实际 n<106n < 10^6 时可计算
  3. 伪多项式:0-1 背包 DP O(nW)O(nW)WW 较小时高效,但 WW 编码位数大时为指数
  4. 平均情况复杂度:某些 NP 完全问题(如 3-SAT)在随机实例上有多项式时间算法
  5. 近似算法:TSP 虽 NP 难,但存在 1.5-近似算法(Christofides 1976)

实际工程考虑

  • 工业界更关注”实际多项式时间”(如 O(n3)O(n^3) 以内)
  • 常数因子、缓存友好性、并行性比渐近复杂度更重要
  • 参数化复杂度(Parameterized Complexity)提供更细粒度的分析

哲学意义

P vs NP 实质是”创造(求解)是否比验证(检查)更难”的问题。若 P = NP,则创造与验证等价,将颠覆我们对”创造性”的理解。


14. 参考文献

14.1 经典教材

  1. Cormen, T. H., Leiserson, C. E., Rivest, R. L., Stein, C. 2022. Introduction to Algorithms (4th ed.). MIT Press. ISBN 978-0262046305. Chapter 1-4, 17.
  2. Kleinberg, J., Tardos, E. 2006. Algorithm Design. Pearson. ISBN 978-0321295354. Chapter 2-5.
  3. Sedgewick, R., Wayne, K. 2011. Algorithms (4th ed.). Addison-Wesley. ISBN 978-0321573513. Chapter 1-2.
  4. Skiena, S. S. 2020. The Algorithm Design Manual (3rd ed.). Springer. ISBN 978-3030542556. Chapter 1-2.
  5. Knuth, D. E. 1968. The Art of Computer Programming, Volume 1: Fundamental Algorithms (3rd ed., 1997). Addison-Wesley. ISBN 978-0201896831. Section 1.1-1.2.
  6. Knuth, D. E. 1973. The Art of Computer Programming, Volume 3: Sorting and Searching (2nd ed., 1998). Addison-Wesley. ISBN 978-0201896855. Section 5.1-5.3.
  7. Aho, A. V., Hopcroft, J. E., Ullman, J. D. 1974. The Design and Analysis of Computer Algorithms. Addison-Wesley. ISBN 978-0201000290.
  8. Papadimitriou, C. H. 1994. Computational Complexity. Addison-Wesley. ISBN 978-0201530827.
  9. Garey, M. R., Johnson, D. S. 1979. Computers and Intractability: A Guide to the Theory of NP-Completeness. W. H. Freeman. ISBN 978-0716710455.

14.2 历史性论文

  1. Turing, A. M. 1936. On Computable Numbers, with an Application to the Entscheidungsproblem. Proceedings of the London Mathematical Society s2-42(1):230-265. DOI:10.1112/plms/s2-42.1.230.
  2. Bachmann, P. 1894. Analytische Zahlentheorie (Analytic Number Theory). B. G. Teubner, Leipzig. Volume 2, page 401.
  3. Landau, E. 1909. Handbuch der Lehre von der Verteilung der Primzahlen (Handbook of the Theory of the Distribution of Primes). B. G. Teubner, Leipzig. Volume 2, page 883.
  4. Hartmanis, J., Stearns, R. E. 1965. On the Computational Complexity of Algorithms. Transactions of the American Mathematical Society 117:285-306. DOI:10.1090/S0002-9947-1965-0170805-7.
  5. Cobham, A. 1965. The intrinsic computational difficulty of functions. Proc. 1964 Int. Cong. for Logic, Methodology and Philosophy of Science, Y. Bar-Hillel (ed.), North-Holland, pp. 24-30.
  6. Edmonds, J. 1965. Paths, trees, and flowers. Canadian Journal of Mathematics 17:449-467. DOI:10.4153/CJM-1965-045-4.
  7. Cook, S. A. 1971. The Complexity of Theorem-Proving Procedures. Proc. 3rd ACM STOC, pp. 151-158. DOI:10.1145/800157.805047.
  8. Karp, R. M. 1972. Reducibility Among Combinatorial Problems. Proc. IBM Symp. on Complexity of Computer Computations, pp. 85-103.
  9. Knuth, D. E. 1976. Big Omicron and Big Omega and Big Theta. SIGACT News 8(2):18-24. DOI:10.1145/1008328.1008329.
  10. Bentley, J. L., Haken, D., Saxe, J. B. 1980. A general method for solving divide-and-conquer recurrences. SIGACT News 12(3):36-44. DOI:10.1145/1008861.1008865.
  11. Sleator, D. D., Tarjan, R. E. 1985. Amortized efficiency of list update and paging rules. Communications of the ACM 28(2):202-208. DOI:10.1145/2786.2793.
  12. Tarjan, R. E. 1985. Amortized computational complexity. SIAM J. Alg. Disc. Meth. 6(2):306-318. DOI:10.1137/0606031.
  13. Rabin, M. O. 1976. Probabilistic algorithms. In: J. F. Traub (ed.), Algorithms and Complexity: New Directions and Recent Results, Academic Press, pp. 21-39.

14.3 工业实现与资源

  1. Stanford University. 2026. CS 161: Design and Analysis of Algorithms. https://web.stanford.edu/class/cs161/ (accessed July 20, 2026).
  2. MIT OpenCourseWare. 2026. 6.006: Introduction to Algorithms. https://ocw.mit.edu/courses/6-006-introduction-to-algorithms-spring-2020/ (accessed July 20, 2026).
  3. Big-O Cheat Sheet. 2026. https://www.bigocheatsheet.com/ (accessed July 20, 2026).
  4. VisuAlgo. 2026. Algorithm Visualization. https://visualgo.net/ (accessed July 20, 2026).

15. 延伸阅读

15.1 理论深入

  • CLRS 第 4 章(主定理证明)、第 17 章(摊还分析)、第 34 章(NP 完全性)
  • Knuth TAOCP Vol.1 §1.2.10(算法分析)、§1.2.11(渐近记号)
  • Papadimitriou 1994 《Computational Complexity》:复杂性类的完整理论
  • Arora-Barak 2009 《Computational Complexity: A Modern Approach》Cambridge University Press, ISBN 978-0521424264:现代复杂性理论教材,涵盖 PCP 定理、交互式证明、量子复杂性类
  • Goldreich 2008 《Computational Complexity: A Conceptual Perspective》Cambridge University Press:从概念视角阐述复杂性理论
  • Motwani-Raghavan 1995 《Randomized Algorithms》Cambridge University Press, ISBN 978-0521474658:随机化算法系统化教材,涵盖 Monte Carlo / Las Vegas 算法
  • Nisan-Shamir 1991 《On the Power of Multiplicative Proofs》STOC:计算复杂性与现代密码学交叉

15.2 应用拓展

  • 高性能工程实现:Linux 内核 lib/sort.c 实现的归并排序、lib/list_sort.c 的链表归并、Python list.sort() 的 Timsort(Tim Peters 2002,归并+插入混合)、Rust std::sort 的 pdqsort(pattern-defeating quicksort, Orson Peters 2021)
  • 数据库查询优化:PostgreSQL 查询优化器的动态规划(Selinger 1979)、连接顺序选择的 System R 动态规划、Spark Catalyst 优化器的基于规则的等价改写
  • 分布式系统一致性:Paxos(Lamport 1998)的多数派投票复杂度 O(n2)O(n^2) 消息、Raft(Ongaro-Ousterhout 2014)的领导者日志复制摊还 O(1)O(1)、ZooKeeper ZAB 协议的顺序广播
  • 机器学习算法分析:Adam 优化器(Kingma-Ba 2015)的收敛性证明使用势能法、SGD 在强凸条件下的 O(1/T)O(1/T) 收敛、Mini-batch SGD 的方差缩减分析
  • 加密协议复杂度:RSA 密钥生成依赖 Miller-Rabin 素性测试 O(klog3n)O(k \log^3 n)、AES 分组加密的 O(n)O(n) 固定时间、SHA-256 哈希的 O(n)O(n) 流式处理
  • 图数据库与图计算:Neo4j 的图遍历 O(V+E)O(V+E)、GraphX 的 Pregel 模型超步复杂度、Dijkstra 在路网中的 AA^* 启发式加速

15.3 工程实现练习

  1. 复杂度自动分析工具(中阶):基于 LLVM IR 静态分析循环嵌套层数与归纳变量,自动推断 OO 复杂度。提示:参考 COSTA / ANTLR 的循环边界分析
  2. 主定理判定器(中阶):输入 a,b,f(n)a, b, f(n),输出主定理适用的情形与最终 Θ\Theta 表达式。需处理 f(n)=nlogbalogknf(n) = n^{\log_b a} \log^k nkk 参数识别
  3. 可视化递归树工具(中阶):对任意 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n) 递推,生成递归树 Mermaid 图,标注每层工作量与总工作量
  4. 摊还分析沙盒(高阶):实现动态数组、二项堆、Splay 树三种数据结构,实时展示势能函数 Φ(D)\Phi(D) 与摊还代价 c^i\hat{c}_i 的演化
  5. LeetCode 刷题进度追踪系统(中阶):基于 Git 提交记录与题目分类,动态生成学习路径推荐,使用 Floyd-Warshall 计算知识点最短学习距离
  6. 算法学习路径推荐引擎(高阶):基于 Bloom 分类法与知识图谱,使用拓扑排序规划学习顺序,使用 Dijkstra 计算最短前置路径

15.4 教学视频与公开课

  • MIT 6.006 Introduction to Algorithms(Erik Demaine 等):渐近分析入门,30 讲完整覆盖数据结构与图算法
  • Stanford CS 161 Design and Analysis of Algorithms(Tim Roughgarden):分治、随机化、流算法的高质量讲解
  • Princeton COS 226 Algorithms(Sedgewick 本人授课):与教材《Algorithms 4th》配套,可视化效果突出
  • UC Berkeley CS 170 Efficient Algorithms and Intractable Problems(Christos Papadimitriou):复杂性理论与 NP 完全性的权威讲解
  • CMU 15-451 Algorithms:CMU 算法课,涵盖高级算法设计与分析
  • MIT 6.046J Design and Analysis of Algorithms(Srini Devadas, Nancy Lynch):6.006 的进阶版本
  • 3Blue1Brown《Essence of Linear Algebra》:数学基础补充,理解矩阵乘法复杂度的几何意义

15.5 进阶主题

  • 平滑分析(Smoothed Analysis):Spielman-Teng 2001《Smoothed Analysis of Algorithms》JACM 52(3):385-463 提出,将最坏与平均分析结合,解释单纯形法的实际高效性
  • 参数化复杂度(Parameterized Complexity):Downey-Fellows 1999《Parameterized Complexity》Monographs in Computer Science,引入 FPT(Fixed-Parameter Tractable)类与 W-hierarchy
  • 在线算法与竞争分析(Online Algorithms & Competitive Analysis):Sleator-Tarjan 1985 引入竞争比,Borodin-El-Yaniv 1998《Online Computation and Competitive Analysis》系统化
  • 近似算法(Approximation Algorithms):Vazirani 2001《Approximation Algorithms》Springer,关注 NP 难问题的多项式时间近似方案 PTAS
  • 量子计算复杂性(Quantum Complexity):Shor 1994 量子傅里叶变换 O((logn)3)O((\log n)^3) 因式分解、Grover 1996 量子搜索 O(n)O(\sqrt{n})、BQP 类
  • 通信复杂度(Communication Complexity):Yao 1979《Some Complexity Questions Related to Distributive Computing》STOC,研究分布式计算的下界
  • 数据流算法(Streaming Algorithms):Muthukrishnan 2005《Data Streams: Algorithms and Applications》:亚线性空间处理
  • 分布式算法(Distributed Algorithms):Peleg 2000《Distributed Computing: A Locality-Sensitive Approach》SIAM,CONGEST 与 LOCAL 模型

16. 总结

16.1 知识图谱总览

graph TD
    A[算法分析基础] --> B[渐近记号 O/Ω/Θ/o/ω]
    A --> C[递推与主定理]
    A --> D[摊还分析]
    A --> E[复杂性类 P/NP/NPC]
    B --> B1[Bachmann 1894]
    B --> B2[Landau 1909]
    B --> B3[Knuth 1976]
    C --> C1[递归树法]
    C --> C2[主定理三情形]
    C --> C3[Akra-Bazzi]
    D --> D1[聚合分析]
    D --> D2[核算方法]
    D --> D3[势能方法]
    E --> E1[Turing 1936]
    E --> E2[Hartmanis-Stearns 1965]
    E --> E3[Cook 1971]
    E --> E4[Karp 1972]
    D3 --> D4[Sleator-Tarjan 1985]
    D3 --> D5[动态数组摊还 O1]
    D3 --> D6[Splay树摊还 O log n]

16.2 三大核心论证方法

  1. 渐近分析(Asymptotic Analysis):通过 O/Ω/ΘO/\Omega/\Theta 刻画算法在输入规模 nn \to \infty 时的增长率。核心是忽略常数与低阶项,聚焦主导项。Bachmann 1894 起源、Knuth 1976 系统化。
  2. 递归树归约(Recursion Tree Reduction):将递归式 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n) 视为树形结构,叶子总工作量 O(nlogba)O(n^{\log_b a}),内部节点总工作量 i=0logbnaif(n/bi)\sum_{i=0}^{\log_b n} a^i f(n/b^i)。主定理三情形正是据此推导。
  3. 势能摊还(Potential Method Amortization):定义势能函数 Φ(D)\Phi(D),将单次操作的真实代价 cic_i 与势能变化 ΔΦ\Delta \Phi 相加得摊还代价 c^i=ci+Φ(Di)Φ(Di1)\hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1})。Sleator-Tarjan 1985 系统化,用于证明动态数组 O(1)O(1) 摊还、Splay 树 O(logn)O(\log n) 摊还等。

16.3 工业级选型决策树

给定问题规模 n:
├── n ≤ 20:暴力回溯 / 状态压缩 DP
├── 20 < n ≤ 100:$O(n^3)$ 多项式算法(Floyd、矩阵乘法)
├── 100 < n ≤ 10^4:$O(n^2)$ 冒泡/选择/插入排序、朴素 DP
├── 10^4 < n ≤ 10^6:$O(n \log n)$ 归并/快排/堆排、二分
├── 10^6 < n ≤ 10^9:$O(n)$ 线性扫描、哈希、基数排序
└── n > 10^9:$O(\log n)$ 二分查找、$O(1)$ 哈希查询、$O(\sqrt{n})$ 数论分块

给定时间限制 T(秒)与单次操作常数 c ≈ 10^8:
- T=1s:可承受操作数 ≈ 10^8
- T=10s:可承受操作数 ≈ 10^9
- 在线服务 T<0.1s:必须 $O(\log n)$ 或 $O(1)$

NP 完全问题应对策略:
├── 小规模 n ≤ 30:分支限界 + 强剪枝
├── 中规模 n ≤ 100:启发式搜索(模拟退火、遗传算法)
├── 大规模 n > 100:近似算法 + PTAS / FPTAS
└── 实时性要求高:贪心 + 局部搜索

16.4 12 项基准自检清单

#基准项本章达成情况
1学习目标✅ 7 条 Bloom 分类法目标,覆盖记忆/理解/应用/分析/评估/对比/创造
2历史动机✅ Turing 1936→Bachmann 1894→Landau 1909→Knuth 1976→Hartmanis-Stearns 1965→Cobham/Edmonds 1965→Cook 1971→Karp 1972→Bentley-Haken-Saxe 1980→Sleator-Tarjan 1985
3形式化定义✅ 五类渐近记号 + 图灵机/RAM 模型 + P/NP/NPC + 主定理 + 递推关系
4理论推导✅ 主定理三情形证明思路 + 摊还分析三方法推导 + 势能法动态数组
5代码示例✅ Python/C++/Java 多语言实现(主定理判定器、势能法、复杂度分析工具)
6对比分析✅ 五类渐近记号对比、三大摊还方法对比、五大算法设计范式对比
7常见陷阱✅ 10 项典型陷阱(O/Θ 混淆、编码方式、常数因子、最坏情况、主定理误用等)
8工程实践✅ 8 项优化技巧 + 工业案例(NumPy/Linux/PostgreSQL)+ 基准测试框架
9案例研究✅ 7 个经典案例(归并/快排/二分/Karatsuba/Strassen/动态数组/Splay)
10习题✅ 5 选择 + 4 填空 + 2 代码修正 + 3 开放论述,含详细答案
11参考文献✅ 26 条 ACM 格式引用(9 教材 + 13 论文 + 4 在线资源)含 DOI
12延伸阅读✅ 5 子节(理论深入 / 应用拓展 / 工程练习 / 教学视频 / 进阶主题)

16.5 学习路径四阶段

graph LR
    S1[阶段1: 入门<br/>1-2 周] --> S2[阶段2: 进阶<br/>3-4 周]
    S2 --> S3[阶段3: 实战<br/>4-6 周]
    S3 --> S4[阶段4: 深造<br/>持续]

    S1 -.->|内容| S1C[渐近记号<br/>主定理<br/>基础排序与查找]
    S2 -.->|内容| S2C[摊还分析<br/>五大设计范式<br/>图算法]
    S3 -.->|内容| S3C[动态规划进阶<br/>NP 完全性<br/>LeetCode 高频题]
    S4 -.->|内容| S4C[高级主题<br/>论文研读<br/>开源贡献]

阶段 1:入门(1-2 周)

  • 阅读 CLRS 第 1-4 章
  • 掌握五类渐近记号 + 主定理三情形
  • 实现 5 大排序算法(冒泡/插入/选择/归并/快排)
  • LeetCode Easy 完成 50 题

阶段 2:进阶(3-4 周)

  • 阅读 CLRS 第 17 章(摊还分析)+ 第 22-24 章(图算法)
  • 掌握动态数组摊还 O(1)O(1)、Splay 树摊还 O(logn)O(\log n)
  • 实现 Dijkstra、Floyd-Warshall、Kruskal、Prim
  • LeetCode Medium 完成 100 题

阶段 3:实战(4-6 周)

  • 阅读 CLRS 第 34 章(NP 完全性)
  • 掌握 DP 状态压缩、位运算优化
  • 实现数独求解器、N 皇后、TSP 近似算法
  • LeetCode Hard 完成 50 题

阶段 4:深造(持续)

  • 阅读 Arora-Barak《Computational Complexity》
  • 研读 STOC/FOCS/SODA 经典论文
  • 参与开源项目(Linux 内核 sort、Redis、PostgreSQL 优化器)
  • 撰写技术博客与算法分析报告

16.6 教学反思

本文档对标 MIT 6.006 / Stanford CS 161 / CMU 15-451 等顶尖课程教材,遵循 FANDEX 内容工程规范的 12 项质量基准。设计要点:

  1. 历史脉络优先:以 Turing 1936 为起点,强调算法分析不是凭空而来,而是计算复杂性理论演进的自然产物。读者理解”为什么需要算法分析”后,再学习”如何分析”。
  2. 形式化与直观并重:每个定理先给出严格数学定义,再通过递归树、势能函数等可视化工具辅助理解。避免”只记结论不懂推导”。
  3. 三方法对比教学:渐近分析、递归树归约、势能摊还三大论证方法贯穿全文。读者应能针对新算法自主选择合适的分析方法。
  4. 工程案例驱动:每个理论点都配有工业级案例(NumPy、Linux 内核、PostgreSQL 优化器、Raft 一致性),避免”空中楼阁”。
  5. 0 基础可达:前置知识仅需高中数学(集合、函数、对数)与一门编程语言基础。所有数学符号首次出现时附中文释义。
  6. 可扩展性留白:进阶主题(平滑分析、参数化复杂度、量子复杂性、通信复杂度、数据流算法)为读者指明后续学习方向,预留深入研究空间。

16.7 版本与维护

  • 创建:2026-05-27
  • 金标准升级:2026-07-20
  • 审阅者:FANDEX Content Engineering
  • 下次审阅:2026-10-20(季度审阅周期)
  • 反馈渠道:通过 GitHub Issues 提交错误报告与改进建议
  • 许可证:CC BY-SA 4.0(知识共享 署名-相同方式共享 4.0 国际)

“算法分析的本质,是用数学语言描述计算过程的资源消耗。从 Turing 1936 的图灵机模型到 Knuth 1976 的渐近记号系统化,从 Hartmanis-Stearns 1965 的复杂性类到 Sleator-Tarjan 1985 的势能摊还,每一步演进都让我们更深刻地理解’什么是好算法’。掌握算法分析,不仅是技能,更是思维方式。”

—— FANDEX 内容工程团队,2026 年 7 月

返回入门指南