Appearance
算法复杂度分析基础
概念
算法复杂度刻画的是"问题规模
- 时间复杂度(time complexity):基本操作的执行次数随
的增长趋势; - 空间复杂度(space complexity):除输入本身外,额外占用的存储空间随
的增长趋势。
关键在"渐进"两个字:复杂度忽略常数因子和低阶项。
大 O 记号(big-O notation)的数学定义:
若存在正常数
和 ,使得当 时恒有 ,则记 。
换成白话:
一句话理解:复杂度不是"跑得多久",而是"数据量翻倍时,耗时涨几倍"。
涨 2 倍, 涨 4 倍, 几乎不涨——这才是分档的意义。
原理
一、四个记号
| 记号 | 名称 | 定义( | 直观 |
|---|---|---|---|
| 渐进上界 | |||
| 渐进下界 | |||
| 紧确界 | |||
| 严格小 | 对任意 |
以
① 证明
代
② 证明
③ 由 ①② 得
④
二、常见复杂度阶与大小关系
| 取值 | 1 | 10 | 1024 | 10240 | 1 048 576 | 1.07×10⁹ | 大得无法写出 |
三条要紧的补充:
- 对数的底数不影响阶:
,换底只差一个常数。所以写 不必标底数。 与 之间仍是 ( 足够大时),但 与 不同阶。 、 属"指数级",通常等价于"不可行"。 时 已接近可接受上限。
三、两条运算规则
加法规则(顺序执行,取大):若一段代码分两段,代价分别是
乘法规则(嵌套/循环相乘):
例:循环体内调用一次
四、最好 / 最坏 / 平均
| 情况 | 含义 | 典型例子 |
|---|---|---|
| 最好 | 最有利输入下的代价 | 直接插入排序正序输入 |
| 最坏 | 最不利输入下的代价 | 快速排序逆序输入 |
| 平均 | 按输入出现概率加权的期望代价 | 顺序查找等概率时 |
408 默认问的是最坏情况;只有题目明确说"平均"时才按等概率期望算。
五、空间复杂度
只算辅助空间,不算输入数据本身占的空间。
| 情形 | 空间复杂度 | 说明 |
|---|---|---|
| 只用了有限个变量 | 冒泡、插入、堆排序 | |
| 开了一个与 | 归并排序的辅助数组 | |
| 递归 | 至少 | 必须把调用栈算进去 |
递归的空间一定要算栈深:递归求
六、递归式的求解:三种方法
分治算法的时间通常写成递归式,例如
1. 代入法(guess & verify)
猜出答案,再用数学归纳法验证。麻烦但通用。
2. 递归树(recursion tree)
把递归展开成一棵树,每层代价相加。
n ← 第 0 层:代价 n
/ \
n/2 n/2 ← 第 1 层:两个 n/2,代价 n
/ \ / \
n/4 n/4 n/4 n/4 ← 第 2 层:四个 n/4,代价 n
... ... ...
每层代价都是 n,共 log2 n + 1 层(到底部 n/2^k = 1)3. 主定理(Master theorem)—— 首选
对形如
的递归式,记临界指数
| 情形 | 条件 | 结论 |
|---|---|---|
| ① | ||
| ② 同阶 | ||
| ③ |
四个必背结果:
| 递归式 | 出处 | 主定理判据 | 结果 |
|---|---|---|---|
| 归并排序、快排平均 | |||
| 遍历二叉树全部结点 | |||
| 折半查找 | |||
| 快排最坏、冒泡 |
最后一条要特别记住:主定理只处理"规模缩小到
"的情形, 这种"每次只少一个元素"的必须手动展开。
示例
例 1:嵌套循环(完整列式)
c
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
s++; /* 基本操作 */第一步,看内层:对固定的
第二步,对
第三步,取最高阶:
结论:
例 2:对数阶循环(完整列式)
c
i = 1;
while (i <= n)
i = i * 2;第一步,列出
第二步,确定循环次数:循环在
第三步:
实测对照:
同理:for (i = 1; i <= n; i *= 2) for (j = 1; j <= n; j++) 是
例 3:四个递归式的完整求解
①
递归树法:每层总代价都是
主定理法:
②
递归树法:第
主定理法:
③
展开:
④
逐层展开:
结论汇总:
例 4:空间复杂度分析
c
/* 版本 A:非递归,只用了 3 个变量 —— 空间 O(1) */
int sumA(int n) {
int s = 0;
for (int i = 1; i <= n; i++) s += i;
return s;
}
/* 版本 B:递归,栈深 n —— 空间 O(n) */
int sumB(int n) {
return n <= 1 ? n : n + sumB(n - 1);
}两个版本时间复杂度都是
例 5:实测操作次数验证复杂度
#include <stdio.h>
/* 统计三种循环结构实际执行了多少次,用数据验证阶的结论 */
int main(void) {
int ns[] = {8, 64, 512, 4096};
printf("%8s %12s %12s %12s %10s\n", "n", "O(n)", "O(n^2)", "O(n log2 n)", "O(log2 n)");
for (int t = 0; t < 4; t++) {
int n = ns[t];
long c1 = 0, c2 = 0, c3 = 0; int c4 = 0;
for (int i = 0; i < n; i++) c1++; /* O(n) */
for (int i = 0; i < n; i++) /* O(n^2) */
for (int j = 0; j < n; j++) c2++;
for (int i = 1; i <= n; i *= 2) /* O(n log n) */
for (int j = 0; j < n; j++) c3++;
for (int i = 1; i <= n; i *= 2) c4++; /* O(log n) */
printf("%8d %12ld %12ld %12ld %10d\n", n, c1, c2, c3, c4);
}
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
按输出反推阶(
| 列 | 8 时的次数 | 4096 时的次数 | 涨幅 | 对应阶 |
|---|---|---|---|---|
| 8 | 4096 | 512 倍 | 线性 | |
| 64 | 16 777 216 | 262 144 倍 = | 平方 | |
| 32 | 53 248 | 1664 倍 ≈ | ||
| 4 | 13 | 3.25 倍 | 对数 |
看涨幅就能定阶:规模涨 512 倍 → 线性阶涨 512 倍、平方阶涨
Python 对照
from math import log2, floor
def count_n(n): return n # 单层循环
def count_n2(n): return n * n # 双层循环
def count_nlogn(n):
c, i = 0, 1
while i <= n: # 外层 log n 次
c += n; i *= 2 # 内层 n 次
return c
def count_logn(n):
c, i = 0, 1
while i <= n:
c += 1; i *= 2
return c
print(f"{'n':>8}{'n':>10}{'n^2':>14}{'nlogn':>12}{'logn':>8}")
for n in (8, 64, 512, 4096):
print(f"{n:>8}{count_n(n):>10}{count_n2(n):>14}{count_nlogn(n):>12}{count_logn(n):>8}")
# 用"翻倍比"看阶:n 翻倍时各类开销涨几倍
print("\n规模翻倍时的涨幅(比值 → 阶):")
for f, name in [(count_n, "O(n)"), (count_n2, "O(n^2)"), (count_nlogn, "O(n log n)"), (count_logn, "O(log n)")]:
r = f(4096) / f(2048)
print(f" {name:>10}: {r:.2f} 倍")
# 递归式验证:T(n)=2T(n/2)+n 的实际基本操作次数
def merge_cost(n):
if n <= 1: return 0
return 2 * merge_cost(n // 2) + n
print("\nT(n)=2T(n/2)+n 实测:", {n: merge_cost(n) for n in (4, 8, 16, 64)})
print("对照 n*log2(n) :", {n: n * log2(n) for n in (4, 8, 16, 64)})
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
考点
考点
1. 全书复杂度速查表(408 综合题的公共素材)
| 结构 / 算法 | 平均 | 最坏 | 空间 | 备注 |
|---|---|---|---|---|
| 顺序表按下标访问 | — | 随机存取 | ||
| 顺序表插入/删除 | 搬元素 | |||
| 单链表按下标访问 | — | 顺序存取 | ||
| 单链表插入/删除(已知位置) | 改指针 | |||
| 栈 / 队列操作 | — | —— | ||
| 二叉树遍历 | 每结点一次 | |||
| 二叉排序树查找 | — | 退化成单枝时最坏 | ||
| AVL 查找 / 插入 | — | 始终平衡 | ||
| 堆:插入 / 删除堆顶 | 取最值 | |||
| 建堆 | 自底向上 | |||
| 图的 DFS / BFS(邻接表) | 邻接矩阵是 | |||
| Prim(邻接矩阵) | 稠密图 | |||
| Kruskal | 稀疏图 | |||
| Dijkstra(邻接矩阵) | 不能有负权 | |||
| Floyd | 全源最短路 | |||
| 拓扑排序 | —— | |||
| 顺序查找 | —— | |||
| 折半查找 | 需有序 + 顺序存储 | |||
| 散列表查找 | 与装填因子 | |||
| 八大排序 | 见 归并排序与基数排序 的总结表 |
2. 阶排序(必背)
3. 主定理判据(三步走)
- 写出
、 、 ,算出 ; - 把
与 比大小(更小 / 同阶 / 更大); - 对应取
、 、或 。
前提检查:
4. 易错点
是上界, 的算法同时也是 。题目问"时间复杂度是多少",答最紧的上界。- 对数底数可省略,
、 、 同阶;但 与 是不同阶, 比 大。 - 空间复杂度不含输入数据。排序算法的"
空间"指辅助空间,不是说输入不占内存。 - 递归必须把栈算进空间:
或 都可能,取决于递归深度。归并排序写 而不是 。 - "平均复杂度"要用概率加权,不是把最好和最坏取算术平均。快排平均是
,不是 。 - 循环里的"乘法"要看清是乘自己还是乘常数:
i *= 2是 ;i += 2是 (仍为 );i++是 。 - 两层循环不一定
:内层边界依赖外层(如例 1)要老老实实求和;内层是 规模就变成 。 - 复杂度与机器无关,但与"基本操作的选取"有关。同一算法按"比较次数"算和按"移动次数"算可能给出不同阶(如简单选择排序:比较
,移动 )。题目说什么就是什么。 - 阶不等于实际快慢:
很小时, 的实现可能因为常数小反而比 快。复杂度只描述增长趋势。
5. 408 常见问法
- "以下代码段的时间复杂度是"→ 直接算循环次数;
- "下列排序算法中,时间复杂度为
且稳定的是"→ 归并(详见 43 篇总结表); - "递归算法的空间复杂度主要取决于"→ 递归深度;
- 综合题里给出递推式让求渐进阶 → 优先用主定理,套不上就展开。
小结
- 复杂度是渐进度量:忽略常数与低阶项,只看增长趋势。
- 四个记号里
是上界、 是下界、 是同阶; 不唯一,答题写最紧的那个。 - 两条规则:顺序取大(加法)、嵌套相乘(乘法);
的底数一律可省。 - 递归式首选主定理:算
,比较 与 ; 的情形( )必须手动展开。 - 空间只算辅助空间,递归必算栈深。
- 数据结构部分到此收口。回顾这条线的脉络:线性结构 → 树形结构 → 图 → 查找 → 排序 → 复杂度,每一步都在回答"用什么方式组织数据,才能让某类操作更快"。而"更快"的极限,最终由计算机底层的存储层次与电路速度决定——这正是下一门课的主题。
下一篇:计算机系统概述(进入 L2 机器层 · 计算机组成原理)
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。