5.3 步数统计与渐近增长
第 5.2 节证明算法是否正确。但正确性只能说明答案对不对,不能说明输入变大后算法是否仍然实用。
假设两个正确搜索过程在规模为 n 的输入上分别需要
20n与n2 次基本操作。当 n=5 时,第二个表达式更小:
20(5)=100,52=25. 当 n=100 时,比较结果反转:
20(100)=2000,1002=10000. 为了理解可扩展性,我们要研究工作量怎样随输入规模。
是描述算法接收多少输入的数,通常记作 n。
例如:
- 对一个整数,规模可以指二进制位数,而不是整数本身的数值。
本节每次使用 n 时都会说明它的含义。
规定要计算哪些基本操作,例如比较、赋值、算术运算或数组访问。
这个计数是数学模型,不是秒表测量。真实机器在处理器速度、语言、编译器和内存行为上都有差异。计算操作次数可以让我们不依赖这些细节比较算法。
考虑:
x ← a + b
y ← x × 2
return y
若计算算术操作与赋值,一种模型会得到:
总计为 4,与输入规模无关。另一种合理模型可能计算 return,或者把算术与赋值视为一次组合操作。精确常数会变化,但“不随输入增长”这一点不变。
考虑:
total ← 0
for i ← 1 to n
total ← total + A[i]
return total
如果只计算循环体中加法的执行次数,精确计数是
如果计算一次初始化、n 次加法、n 次给 total 赋值和一次返回,则
T(n)=1+n+n+1=2n+2. 代价模型改变精确公式,但两个公式都与 n 成比例增长。
如果一个循环运行 n 次,后面的另一个循环运行 3n 次,循环体总执行次数为
因为第二块在第一块之后运行,所以顺序工作量相加。
考虑:
for i ← 1 to n
for j ← 1 to n
output (i, j)
对 i 的每个取值,内层循环都会使用 j 的全部 n 个取值。因此 output 执行
n 项n+n+⋯+n=n2 次。
这些迭代对应笛卡尔积
{1,…,n}×{1,…,n}, 这与第 1 章建立了联系。
并非每个嵌套循环都是 n2。如果内层循环只从 1 运行到 i,计数为
1+2+⋯+n=i=1∑ni=2n(n+1). 它仍然是二次增长,但精确数量大约只有 n2 的一半。
考虑:
x ← n
while x > 1
x ← floor(x / 2)
执行 k 次后,x 大约为
当这个值到达 1 时循环停止。解不等式
2kn≤1 得到
n≤2k⟹k≥log2n. 在许多常见边界约定下,迭代次数为
⌊log2n⌋, 至多相差一个小的端点调整。重要结论是对数增长:把 n 加倍,迭代次数大约只增加一次。
在实时操作计数器下运行单层、顺序、嵌套、三角形与减半循环。改变 n,逐步观察被高亮的执行位置,再把实际轨迹与从可视迭代区域拼出的精确符号计数进行比较。
同一个输入规模可能产生不同代价。
对长度为 n 的数组进行线性搜索:
- 如果目标不存在,或只出现在索引 n−1,算法要做 n 次比较。
因此:
Tbest(n)=1,Tworst(n)=n. 每次陈述复杂度时,都应说明它描述最好情况、最坏情况还是其他指定情形。本课程中,如果没有额外说明,上界分析通常指最坏情况。
假设一个实现使用
T1(n)=3n2+10n+7, 另一个使用
T2(n)=50n2+2. 精确计数不同,但当 n 很大时,二者都被 n2 的常数倍控制。二次项最终增长得比线性项与常数项更快。
描述这种长期增长,并忽略常数因子与低阶项。
如果存在常数 c>0 与 n0,使得对每个 n≥n0 都有
0≤f(n)≤cg(n), 就写成
f(n)∈O(g(n)). 常数 c,n0 是见证。不等式不必对每个小输入成立,只需要从某个阈值以后一直成立。
示例:证明二次上界
令
f(n)=3n2+10n+7. 当 n≥1 时,
10n≤10n2且7≤7n2. 因此
f(n)≤3n2+10n2+7n2=20n2. 选择 c=20,n0=1 就证明
f(n)∈O(n2). 大 O 是上界,不一定是紧确描述。因为当 n≥1 时 n≤n2,线性函数也属于 O(n2)。要表达“恰好按这个阶增长”,需要匹配的上界与下界。
如果存在常数 c>0 与 n0,使得对每个 n≥n0 都有
0≤cg(n)≤f(n), 就写成
f(n)∈Ω(g(n)). 对 f(n)=3n2+10n+7 且 n≥1,
3n2≤f(n), 所以取 c=3 得到 f(n)∈Ω(n2)。
当
f(n)∈O(g(n))且f(n)∈Ω(g(n)) 同时成立时,写成
f(n)∈Θ(g(n)). 等价地,存在正数 c1,c2,n0,使对每个 n≥n0,
0≤c1g(n)≤f(n)≤c2g(n). 前面的两个界证明了
3n2+10n+7∈Θ(n2). 从慢到快,常见非负增长率为:
1≺logn≺n≺nlogn≺n2≺n3≺2n≺n!. 这里的 ≺ 非正式表示“最终增长得更慢”。它不是第 4 章的偏序关系,虽然二者都表达比较。
增长差距会变得非常夸张。当 n=20 时:
n2=400,2n=1,048,576,n!=2,432,902,008,176,640,000. 常数对真实工作负载仍然重要,尤其在输入较小时。渐近增长回答的是另一个问题:如果输入规模继续增加,会发生什么?
把带有可调常数因子的函数发射到对数增长赛道。移动输入规模地平线,观察交叉点,并拟合上下包络,从而区分宽松的大 O 陈述与紧确的大 Theta 分类。
大 O 本身既不表示“等于”,也不自动表示“最坏情况”。它是一种最终上界关系。输入情形要另行说明;当上下界匹配时,应使用大 Theta。
第 5 章把过程与数学连接起来。我们用伪代码描述算法,用不变量与终止度量证明正确性,计算精确操作次数,并分类长期增长。
第 6 章将发展系统计数原理。这些工具让我们不必逐项列举就能计算选择数量,也会解释嵌套循环、搜索空间和组合算法为什么会增长得如此迅速。