Skip to content
NotesAlgorithm

算法设计与分析课程笔记,覆盖分治、图算法、网络流、线性规划与复杂度基础。

Master Theorem ​

  • If T(n)=aT(nb)+O(nd), then T(n)={O(nd)a<bdO(nlogb⁡a)a>bdO(ndlog⁡n)a=bd

证明思路 / 推导过程 ​

对于递推式 T(n)=aT(nb)+O(nd):

  • 递归深度:共有 logb⁡n 层。
  • 每层合并代价(Combine Cost):O(nd)。
  • 具体到各层代价:
    • 第 0 层:O(nd)
    • 第 1 层:a⋅O((nb)d)=(abd)O(nd)
    • 第 2 层:a2⋅O((nb2)d)=(abd)2O(nd)
    • ...
    • 第 logb⁡n 层:alogb⁡n⋅O(1)=O(nlogb⁡a)

总代价:

T(n)=O(nd)[1+(abd)+(abd)2+⋯+(abd)logb⁡n]

这是一个公比为 r=abd 的等比数列:

  1. 若 a<bd (r<1): 数列收敛,总和由第一项(根节点)主导:T(n)∼O(nd)
  2. 若 a>bd (r>1): 数列发散,总和由最后一项(叶子节点)主导:T(n)=O(nd)⋅(abd)logb⁡n+1−1abd−1=O(nlogb⁡a)
  3. 若 a=bd (r=1): 每层代价相等,共有 logb⁡n+1 层:T(n)=O(ndlogb⁡n)

应用实例:寻找第 k 小的数 (QuickSelect) ​

使用快排(QuickSort)的思路:先进行一次 Partition,找到第 1 个位置 O(n),然后根据 k 的位置选择其中一边进行递归。

1. 最坏情况 (Worst Case) ​

如果每次选出的 pivot 只能筛掉一个元素(例如数组已排序且选第一个元素为 pivot):

T(n)≤O(n)+T(n−1)T(n)=O(n)+O(n−1)+⋯+O(1)=O(n2)

这种情况非常糟糕。

2. 平均情况 (Average Case) ​

我们可以通过概率分析来观察平均性能。定义“好运气”和“坏运气”:

  • Good luck:划分后的子问题规模 ≤34n。
  • Bad luck:划分后的子问题规模 >34n。

在随机选择 pivot 的情况下,落在中间 [14n,34n] 范围内的概率为 1/2(即 Good luck 的概率 p=1/2)。

推导过程: 设 τ(n) 为将规模从 n 缩减到 34n 所需的操作次数。因为每次划分有 1/2 的概率成功缩减,所以 τ(n) 服从参数为 1/2 的几何分布:

E[τ(n)]=2⋅O(n)=O(n)

递归期望代价:

E[T(n)]=E[τ(n)+T(34n)]E[T(n)]=O(n)+E[T(34n)]

展开级数:

E[T(n)]=O(n)[1+34+(34)2+…]

由于这是一个公比为 3/4<1 的收敛等比数列:

E[T(n)]=O(n)

结论:QuickSelect 的平均时间复杂度为 O(n)。

3. 如何选择一个好的 Pivot? (Median of Medians) ​

随机化分析的缺点: 随机性太强,虽然期望是 O(n),但存在最坏情况,且分析达到 O(n) 的概率分布比较麻烦。

我们希望寻找一种确定性的方法来选择 pivot,使得:

T(n)=T(c⋅n)+findpivot+O(n)

启发式方法:中位数的中位数 (BFPTR 算法) ​

  1. 将 n 个数分为 ⌈n/5⌉ 组,每组 5 个数。
  2. 找出每组的中位数,总耗时 O(n)。
  3. 递归地找出这 n/5 个中位数的中位数,记为 x。此步骤代价为 T(n/5)。

Pivot 的质量保证 ​

根据“中位数的中位数”的性质,至少有 3/10 的元素比 x 大,同时也至少有 3/10 的元素比 x 小。 因此,划分后剩下的子问题规模最大为 7n/10。

由此得到递推式:

T(n)=T(n/5)+T(7n/10)+O(n)

复杂度证明 ​

方法一:递归树分析

  • Level 0: n
  • Level 1: 0.2n+0.7n=0.9n
  • Level 2: (0.2+0.7)⋅0.9n=0.81n
  • ...
  • Level k: 0.9kn

总代价为公比为 0.9 的等比级数:

T(n)=n∑k=0∞(0.9)k=n1−0.9=10n=O(n)

方法二:归纳法 (代入法) 假设 T(n)≤Bn:

T(n)=T(0.2n)+T(0.7n)+CnT(n)≤0.2Bn+0.7Bn+Cn=0.9Bn+Cn

若要满足 0.9Bn+Cn≤Bn,则需 Cn≤0.1Bn,即 B≥10C。

实际评价: 虽然理论上是确定的 O(n),但由于常数项 B 较大,实际运行速度往往不如随机取 pivot 的 QuickSelect。

4. 深入讨论:为什么选择 5 作为分组大小? ​

在 BFPTR 算法中,分组大小 g 的选择至关重要。

(1) 为什么选择奇数? ​

选择奇数是因为在对每组进行排序后,中位数是唯一的(即正中间的那个位置)。如果选择偶数(如 4 或 6),则需要规定取左中位数或右中位数,增加了逻辑复杂度。

(2) 为什么是 5 而不是 3? ​

从递推式出发,设分组大小为 g(g 为奇数)。

  • 寻找中位数的中位数需要 T(n/g)。
  • 划分后,根据“中位数的中位数”的几何性质,能够排除的元素个数约为 12⋅g+12⋅ng=g+14gn。
  • 剩余的子问题规模为 n−g+14gn=3g−14gn。

得到递推式:

T(n)=T(1gn)+T(3g−14gn)+O(n)

为了使 T(n)=O(n),必须满足两个系数之和小于 1:

1g+3g−14g<1

解不等式:

4+3g−14g<1⟹3g+34g<1⟹3g+3<4g⟹g>3
  • 若 g=3:系数之和 13+23=1。此时递推式变为 O(nlog⁡n),无法达到线性复杂度。
  • 若 g=5:系数之和 15+710=910=0.9<1。这是满足线性复杂度的最小奇数。

(3) 为什么是 5 而不是 7 或更大? ​

虽然从数学上看,g 越大,剩余子题目的规模比例越小(例如 g=7 时,系数之和约为 0.857<0.9),但实际算法开销由两部分组成:

  1. 递归子问题的规模(随 g 增大而减小)。
  2. 寻找每组中位数的时间开销(随 g 增大而显著增加)。

开销分析:

  • 当 g=5 时:寻找 5 个数的中位数非常快,只需要最多 6 次比较。
  • 当 g=7 时:寻找 7 个数的中位数所需的比较次数和操作复杂度会显著上升。

随着 g 的增大,我们在每一层递归中为了“选出更好的 pivot”所付出的 O(n) 预处理代价(即常数项 C)会迅速膨胀,这会抵消掉子问题规模缩小带来的收益。

结论: 5 是一个“甜点位” (Sweet Spot):它既保证了递推式系数之和小于 1(打破了 O(nlog⁡n) 的魔咒),又保持了极低的分组处理常数。

因此,5 是在平衡“排除效率”与“算法常数”后的最优选择。