Skip to content
NotesAlgorithm

算法设计与分析课程笔记,覆盖分治、图算法、最短路与复杂度基础。

Master Theorem

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

证明思路 / 推导过程

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

  • 递归深度:共有 logbn 层。
  • 每层合并代价(Combine Cost):O(nd)
  • 具体到各层代价
    • 第 0 层:O(nd)
    • 第 1 层:aO((nb)d)=(abd)O(nd)
    • 第 2 层:a2O((nb2)d)=(abd)2O(nd)
    • ...
    • logbn 层:alogbnO(1)=O(nlogba)

总代价

T(n)=O(nd)[1+(abd)+(abd)2++(abd)logbn]

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

  1. a<bd (r<1): 数列收敛,总和由第一项(根节点)主导:T(n)O(nd)
  2. a>bd (r>1): 数列发散,总和由最后一项(叶子节点)主导:T(n)=O(nd)(abd)logbn+11abd1=O(nlogba)
  3. a=bd (r=1): 每层代价相等,共有 logbn+1 层:T(n)=O(ndlogbn)

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

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

1. 最坏情况 (Worst Case)

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

T(n)O(n)+T(n1)T(n)=O(n)+O(n1)++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)]=2O(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(cn)+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)=nk=0(0.9)k=n10.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+CnBn,则需 Cn0.1Bn,即 B10C

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

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

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

(1) 为什么选择奇数?

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

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

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

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

得到递推式:

T(n)=T(1gn)+T(3g14gn)+O(n)

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

1g+3g14g<1

解不等式:

4+3g14g<13g+34g<13g+3<4gg>3
  • g=3:系数之和 13+23=1。此时递推式变为 O(nlogn),无法达到线性复杂度。
  • 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(nlogn) 的魔咒),又保持了极低的分组处理常数。

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