1.2 算法与复杂度分析
算法的五个特性与评价标准,学会用大 O 记号分析时间复杂度与空间复杂度。
考查要求 ★★★ 重点掌握 算法五特性时间复杂度空间复杂度递归复杂度
什么是算法
一个算法必须具备以下五个特性:
| 特性 | 含义 | 反例 |
|---|
| 有穷性 | 必须在执行有穷步之后结束,且每一步都可在有穷时间内完成 | 操作系统是”程序”而非算法,它会一直运行 |
| 确定性 | 每条指令有确切含义,相同输入只能得到相同输出 | “把 x 加上一个比较小的数” |
| 可行性 | 操作都能通过已实现的基本运算执行有限次实现 | “求出 π 的全部小数位” |
| 输入 | 有零个或多个输入 | — |
| 输出 | 有一个或多个输出 | 没有输出的算法毫无意义 |
“好”的算法应达到以下目标:正确性、可读性、健壮性(对非法输入能做出适当反应)、高效率与低存储量需求。其中效率和存储量,就是用时间复杂度和空间复杂度来度量的。
时间复杂度
从”语句频度”到”渐近记号”
一条语句的频度是指它在算法中被重复执行的次数。算法中所有语句频度之和记为 T(n),它是问题规模 n 的函数。
我们并不关心 T(n) 的精确值,而是关心当 n→∞ 时它的增长趋势。通常只需考察基本运算(最深层循环内的语句)的频度 f(n),并取其数量级:
T(n)=O(f(n))
其数学含义是:存在正常数 C 和 n0,使得当 n≥n0 时都满足 0≤T(n)≤C⋅f(n)。也就是说,O 给出的是增长率的上界。
两条运算规则
- 加法规则:T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n))) —— 顺序执行的代码,取最大的那部分。
- 乘法规则:T(n)=T1(n)×T2(n)=O(f(n))×O(g(n))=O(f(n)×g(n)) —— 嵌套的代码,复杂度相乘。
常见复杂度的比较
拖动下面的滑块,直观感受它们的增长速度差异。勾选”对数纵轴”可以同时观察指数阶与多项式阶:
当 n = 20 时:n² = 400,而 2ⁿ = 1M。指数阶在 n 稍大时就会"爆炸"——这就是为什么我们追求多项式甚至对数级算法。
最坏、平均与最好情况
同一个算法,对不同的输入实例,执行次数可能不同。例如在数组中顺序查找值 x:
- 最好时间复杂度:
x 恰好在第一个位置,O(1);
- 最坏时间复杂度:
x 在最后或不存在,O(n);
- 平均时间复杂度:所有输入等概率出现时的期望,约 (n+1)/2 次比较,O(n)。
动手分析:数一数执行次数
分析循环程序的通用方法:设基本语句执行了 t 次,找出循环变量与 t 的关系,再由循环结束条件解出 t。
下面的动画会真实地”运行”四段经典代码,每执行一次基本语句就画一个方块。切换代码和 n 的值,观察次数的增长规律:
动画演示数一数:基本语句执行了多少次?
C 代码
1int count = 0;
2for (int i = 1; i <= n; i *= 2)
3 count++; // 基本语句
1/12n = 16。我们来数一数基本语句(count++ / s+=i)被执行了多少次。
对照动画,我们把四种模式整理成”套路”:
模式 1:循环变量倍增
for (i = 1; i <= n; i *= 2)
count++;
设执行 t 次,则第 t 次时 i=2t−1≤n,得 t≤log2n+1,所以 T(n)=O(log2n)。
模式 2:内层循环次数依赖外层变量
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
count++;
T(n)=i=1∑nj=1∑i1=i=1∑ni=2n(n+1)=O(n2)
模式 3:外层倍增、内层随之增长(易错!)
for (i = 1; i <= n; i *= 2)
for (j = 1; j <= i; j++)
count++;
很多同学会想”外层 logn 次 × 内层 n 次 = O(nlogn)“——错!内层次数是 i,而 i 取 1,2,4,…,2k(2k≤n),因此
T(n)=1+2+4+⋯+2k=2k+1−1<2n⇒T(n)=O(n)
模式 4:累加直到超过 n
i = 0; s = 0;
while (s < n) { i++; s = s + i; }
执行 t 次后 s=1+2+⋯+t=t(t+1)/2,令其 ≥n,得 t≈2n,故 T(n)=O(n)。
同理,while (i * i <= n) i++; 的复杂度也是 O(n)。
递归程序的时间复杂度
递归算法的分析通常先写出递推式,再展开求解。例如求阶乘:
int fact(int n) {
if (n <= 1) return 1; // 递归出口
return n * fact(n - 1); // 规模减 1
}
T(n)=T(n−1)+O(1),展开得 T(n)=T(1)+(n−1)⋅O(1)=O(n)。
再如归并排序:T(n)=2T(n/2)+O(n),展开 log2n 层,每层合计 O(n),故 T(n)=O(nlog2n)。
图 1-3 T(n)=2T(n/2)+n 的递归树:右侧数字是每层工作量之和
空间复杂度
算法的空间复杂度 S(n) 定义为该算法所需的存储空间,也是问题规模 n 的函数:S(n)=O(g(n))。
- 只考虑除输入数据本身以外的辅助空间。
- 若算法所需辅助空间为常数,即 O(1),称算法原地工作(in-place)。
- 递归程序要计入递归工作栈的深度:例如上面的
fact(n) 递归深度为 n,空间复杂度为 O(n)。
即时练习
1下列程序段的时间复杂度是( )。
x = 2;
while (x < n / 2)
x = 2 * x; 2求整数 n(n ≥ 0)阶乘的递归算法如下,其时间复杂度是( )。
int fact(int n) {
if (n <= 1) return 1;
return n * fact(n - 1);
} 3下列程序段的时间复杂度是( )。
count = 0;
for (k = 1; k <= n; k *= 2)
for (j = 1; j <= n; j++)
count++; 4下列程序段的时间复杂度是( )。
int sum = 0;
for (int i = 1; i < n; i *= 2)
for (int j = 0; j < i; j++)
sum++; 5某算法的时间复杂度为 O(n²),表明该算法的( )。
学完了吗? 标记完成后,侧栏会显示进度。进度仅保存在本浏览器中。