Skip to content

如何分析一段代码的复杂度

本节把前面的概念落到具体代码分析上。

学习目标是掌握一个基本流程:先确定输入规模,再找关键操作,然后分析循环、嵌套循环、递归或额外空间,最后用大 O 表示法简化结果。


可以按下面的顺序分析一段代码:

  1. 明确输入规模是什么,例如数组长度 nn、字符串长度 nn,或者图中的顶点数 VV 和边数 EE

  2. 找出关键操作,例如比较、访问、插入、删除或输出。

  3. 分析关键操作会执行多少次。

  4. 如果有多段代码顺序执行,把成本相加,再保留增长最快的部分。

  5. 如果有嵌套循环,通常要观察每层循环次数之间的关系。

  6. 如果有递归,要分析递归调用次数、递归深度和每层额外工作量。

  7. 最后用大 O 表示法忽略常数和低阶项。

这套步骤不是机械公式,而是帮助你避免遗漏关键信息。


如果代码只执行固定数量的语句,而且这些语句不随输入规模增长,那么时间复杂度通常是 O(1)O(1)

读取一个变量
修改一个变量
返回一个结果

无论输入集合有多大,只要这几步本身没有遍历集合,就可以看作常数级操作。


下面的代码会统计正数个数。它必须检查每个元素一次,所以时间复杂度是 O(n)O(n)

int countPositive(const vector<int>& numbers) {
int count = 0;
for (int number : numbers) {
if (number > 0) {
count++;
}
}
return count;
}

这里的关键操作是判断 number > 0。数组中有 nn 个元素,判断就会执行 nn 次。


如果一段代码先遍历一次数组,再进行两层嵌套循环,可以先把它们的成本相加。

第一段:n 次
第二段:n × n 次
总成本:n + n²

根据大 O 简化规则,最终保留增长更快的 n2n^2,所以复杂度是 O(n2)O(n^2)


看到嵌套循环时,不要机械地认为一定是 O(n2)O(n^2)。关键要看每层循环到底执行多少次。

如果外层循环执行 nn 次,内层循环每次也执行 nn 次,通常是 O(n2)O(n^2)

但如果内层循环每次都把变量翻倍,例如从 1、2、4、8 一直增长到 nn,内层可能是 O(logn)O(\log n)。如果外层是 nn 次,总复杂度就可能是 O(nlogn)O(n \log n)


递归复杂度需要分析两个问题:

  • 递归会调用多少层。
  • 每一层除了递归调用之外还做了多少额外工作。

例如,一个递归函数每次把问题规模减半,并且每层只做常数级工作,常见复杂度可能是 O(logn)O(\log n)

如果每层都要遍历当前规模的数据,同时又把问题拆成多个子问题,复杂度就可能更高。后续学习递归与分治时,会更系统地分析这类情况。


下面的代码会枚举所有元素对,因此时间复杂度是 O(n2)O(n^2)

void printPairs(const vector<int>& numbers) {
int n = numbers.size();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cout << numbers[i] << ", " << numbers[j] << endl;
}
}
}

外层循环执行 nn 次。对于外层的每一次,内层循环又执行 nn 次。因此总执行次数大约是 n2n^2


分析代码复杂度时,要先明确输入规模和关键操作,再根据顺序结构、循环结构、嵌套结构和递归结构判断增长趋势。

不要只凭代码长短判断效率。短代码也可能很慢,长代码也可能只是常数级操作。真正重要的是关键操作会随着输入规模增长多少次,以及是否额外占用了随规模增长的空间。