C++ 进阶 — 第 5 课
不是精确时间(秒),是「增长趋势」—— 数据量翻倍,操作次数翻几倍?
| 记号 | 名字 | n=1000 时大概次数 | n 翻 10 倍 |
|---|---|---|---|
| O(1) | 常数 | 1 | 不变 |
| O(log n) | 对数 | ~10 | +10 |
| O(n) | 线性 | 1000 | ×10 |
| O(n log n) | 线性对数 | ~10000 | ×20 |
| O(n²) | 平方 | 1,000,000 | ×100 |
| O(2ⁿ) | 指数 | 天文数字 | 💀 |
这张表怎么读:只看最右边两列。n 从 1000 涨到 10000(翻 10 倍),O(n) 的工作量也翻 10 倍,O(n²) 翻 100 倍,而 O(log n) 只多了 10 次 —— 这就是「阶」的差别。
int a = v[5]; // 数组按下标取值
st.push(x); // 栈顶操作
cout << pq.top(); // 堆顶查看
for (int i = 0; i < n; i++) // 一层循环
sum += v[i];
v.push_back(x); // vector 末尾追加
for (int i = 0; i < n; i++) // 冒泡排序
for (int j = 0; j < n; j++)
...
sort(v.begin(), v.end()); // 标准排序
pq.push(x); // 堆操作单次 O(log n),n 次就是 O(n log n)
// P1036 选数(选/不选每个元素)
void dfs(...) {
dfs(idx+1, ...); // 选
dfs(idx+1, ...); // 不选 → 每层分2叉,共 2ⁿ 条路
}
数循环只问两个问题:外层跑几次、内层每次跑几次。乘起来,再把常数和低次项丢掉,就是阶。下面六段,一段一段数。
for (int i = 0; i < n; i++) // 外层:跑 n 次
sum += a[i]; // 里面:做 1 件事
外层跑 n 次,内层每次只做一件固定的事(O(1))。合起来 n × 1 = O(n)。
for (int i = 0; i < n; i++) cnt++; // 第一段:n 次
for (int j = 0; j < n; j++) cnt++; // 第二段:n 次
这是加法不是乘法:n + n = 2n。系数 2 丢掉,还是 O(n)。规则是「并列取最大」—— 如果一段是 n、一段是 n²,答案就是 O(n²),小的那一段被吃掉。
for (int i = 0; i < n; i++) // 外层:n 次
for (int j = 0; j < n; j++) // 内层:每次都跑满 n 次
cnt++;
外层每走一步,内层就跑一整圈 n 次。n × n = O(n²)。这就是冒泡、选择、插入这些双重循环的阶。
for (int i = 0; i < n; i++) // 外层:n 次
for (int j = 0; j < i; j++) // 内层:跑 i 次,i 从 0 到 n-1
cnt++;
内层的次数是 0 + 1 + 2 + … + (n−1) = n(n−1)/2。虽然只有一半,但 n(n−1)/2 里最高的项还是 n²,系数 1/2 丢掉 → O(n²)。「内层少跑一半」不等于降一阶。
for (int i = n; i > 0; i /= 2) // 循环变量每次变成一半
cnt++;
n → n/2 → n/4 → … → 1。数「除几次才到 1」就是 log₂n,所以是 O(log n)。对照第 1 节那张表:n = 1000 时 log₂1000 差不多就是 10,和我们写的「~10」对得上。
for (int i = 0; i < n; i++) // 外层:n 次
for (int j = n; j > 0; j /= 2) // 内层:log n 次
cnt++;
外层 n 次 × 内层 log n 次 = O(n log n)。sort 之所以是这个阶,就是因为它做了大约 log n 轮、每轮扫 n 个元素。
| 代码长什么样 | 外层跑几次 | 内层跑几次 | 合起来 | 阶 |
|---|---|---|---|---|
单层 i < n | n | 1 | n | O(n) |
| 两段并列 | n + n | — | 2n | O(n) |
双层都 < n | n | n | n² | O(n²) |
内层 j < i | n | i | n(n−1)/2 | O(n²) |
i /= 2 | log₂n | 1 | log₂n | O(log n) |
外层 n、内层 /= 2 | n | log₂n | n log₂n | O(n log n) |
把前面的结论收在一起。这张表只有量级,不写毫秒 —— 同一阶换台机器能差好几倍,能比的只有「数据量翻 10 倍,工作量变几倍」。
| 阶 | 名字 | n 翻 10 倍 | 本课出现过的例子 |
|---|---|---|---|
| O(1) | 常数 | 不变 | 数组按下标取值、栈顶操作、堆顶查看 |
| O(log n) | 对数 | +10 | 每次砍一半的循环(第 3 节⑤)、set / map 查找 |
| O(n) | 线性 | ×10 | 一层循环扫一遍(第 3 节①) |
| O(n log n) | 线性对数 | ×20 | sort、n 次堆操作(第 3 节⑥) |
| O(n²) | 平方 | ×100 | 两层都跑满的嵌套循环(第 3 节③④) |
| O(2ⁿ) | 指数 | 💀 | 选 / 不选的暴力枚举 |
最后一行没有「翻 10 倍」可言:数据每多一个,工作量就翻一倍,很快就谁也跑不动了。所以看到指数,先回头问一句「n 到底多大」。
| 数据结构 | 末尾增删 | 中间增删 | 查找 |
|---|---|---|---|
vector | O(1) ✅ | O(n) ❌ | O(n) |
set | O(log n) | O(log n) | O(log n) |
map | O(log n) | O(log n) | O(log n) |
stack | O(1) | — | — |
queue | O(1) | — | — |
priority_queue | O(log n) | — | O(1)(堆顶) |
同理 —— 额外用了多少内存。日常记两个:
vector<int> v(n); // O(n) — 存 n 个
int memo[1001]; // O(1) — 固定大小,不随输入增长
注意「额外」两个字:输入数据本身占的那份不算,只算你为了算答案另外开的空间。memo[1001] 写死了一千个格子,输入再大也不涨,所以是 O(1)。
n = 100000, 二重循环 for(i) for(j) → O(n²) = 10¹⁰ 次 → 超时 💀
n = 100000, sort → O(n log n) ≈ 1.7×10⁶ 次 → 轻松 ✅
n = 20, dfs 选/不选 → O(2²⁰) ≈ 10⁶ 次 → 能过 ✅
n = 1000, 二重循环 → O(n²) = 10⁶ 次 → 能过 ✅
这四行就是「先看 n 的范围,再看自己的阶」的完整流程。同样是二重循环,n = 1000 能过,n = 100000 就必超时 —— 阶没变,变的是 n。
i /= 2 是 O(log n)),两层也不一定 O(n²)(内层只跑固定几次,合起来还是 O(n))。int memo[1001] 是 O(1),vector<int> v(n) 才是 O(n)。for (int i = n; i > 0; i /= 2) cnt++; 的复杂度是?for (i < n) for (j < i) cnt++; 的复杂度是?for (char c : s)),数一数它是几阶;再想想为什么它没有跟着变慢。n(n−1)/2 代几个 n(10、100、1000)算出来,和 n²/2 比一比,看看差多少比例 —— 你会明白为什么系数可以扔。i++、i /= 2、还是别的?i /= 2 是 O(log n);内层 j < i 仍是 O(n²);int memo[1001] 的空间是 O(1)。new / delete[] 把它讲具体。