面试中如何判断算法的时间复杂度和空间复杂度?
复杂度分析用于描述:**当输入规模增长时,算法的运行时间和额外空间如何增长。**它关注增长趋势,而不是某台机器上的具体毫秒数。
先明确输入规模 n 是什么:
m 和列数 n;V 和边数 E。大 O 记号表示渐进上界,通常忽略常数和低阶项:
这里的“忽略常数”不是说常数在真实性能中没有意义,而是复杂度用于比较规模增长趋势。
| 复杂度 | 直观理解 | 常见例子 |
|---|---|---|
O(1) | 操作次数与输入规模无关 | 数组按下标访问、哈希表平均查找 |
O(log n) | 每次把问题规模缩小一部分 | 二分查找、平衡树查找 |
O(n) | 遍历一次输入 | 数组扫描、链表遍历 |
O(n log n) | 分治并在每层线性处理 | 归并排序、平均快排 |
O(n²) | 两层相互关联的遍历 | 简单排序、枚举所有数对 |
O(2^n) | 每一步产生多个分支 | 未记忆化的子集/递归枚举 |
O(n!) | 枚举所有排列 | 全排列暴力搜索 |
增长速度通常满足:
总复杂度是 O(n + n),去掉常数后仍是 O(n)。
如果两个循环规模不同,应保留不同变量:O(m + n)。
外层执行 n 次,内层每次执行 n 次,总复杂度为 O(n²)。
如果内层规模不依赖外层,也可以是 O(mn):
O(log n)每次循环都把规模减半,因此循环次数约为 log₂n。
O(log n)变量序列为 1, 2, 4, 8...,达到 n 需要 O(log n) 次。
O(n²) 的精确表达执行次数约为 n(n - 1) / 2,去掉常数和低阶项后是 O(n²)。
O(1);O(n);O(n)。面试中应明确说明自己分析的是最好、平均还是最坏情况,默认通常讨论最坏复杂度。
问题规模每次减半,时间复杂度是 O(log n)。
归并排序可以理解为:树高 log n,每层合并总共处理 n 个元素,因此是 O(n log n)。
递归树的节点数量呈指数增长,通常为 O(2^n)。如果增加记忆化,把重复子问题结果保存下来,复杂度可能降为多项式或 O(n),需要重新分析状态数量和每个状态的转移成本。
空间复杂度通常指额外空间,不包含题目已经提供的输入数据;如果题目要求返回结果,是否把结果空间计入要说明口径。
O(1)这里只有几个变量,不随 n 增长。
O(n)集合最多保存 n 个元素,因此额外空间是 O(n)。
如果树退化成链,递归深度为 n,空间复杂度最坏为 O(n);如果树平衡,递归栈约为 O(log n)。
因此“递归算法没有额外数组,所以空间是 O(1)”通常是错误的。
原地算法通常表示只使用 O(1) 或较少的额外工作空间,但不一定意味着输入完全不能修改。分析时要区分:
某一次操作可能很慢,但一长串操作的平均成本很低,这时使用摊销复杂度。
例如动态数组扩容:
push 是 O(1);O(n);n 次 push 的总成本是 O(n);O(1)。摊销复杂度不是把最坏情况忽略,而是分析一系列操作的总成本。
以下是常见实现的典型复杂度,哈希表的查找、插入和删除是平均情况:
| 数据结构 | 访问 | 查找 | 插入 | 删除 | 备注 |
|---|---|---|---|---|---|
| 数组 | O(1) | O(n) | 尾部摊销 O(1) | 头部 O(n) | 中间插入会移动元素 |
| 链表 | O(n) | O(n) | 已知节点 O(1) | 已知节点 O(1) | 找到节点本身仍可能是 O(n) |
| 哈希表 | - | 平均 O(1) | 平均 O(1) | 平均 O(1) | 最坏可能退化 |
| 平衡搜索树 | O(log n) | O(log n) | O(log n) | O(log n) | 依赖平衡维护 |
| 堆 | 查看堆顶 O(1) | O(n) | O(log n) | 删除堆顶 O(log n) | 适合优先队列 |
| 算法 | 时间复杂度 | 额外空间 | 说明 |
|---|---|---|---|
| 线性查找 | 最坏 O(n) | O(1) | 无序数据也可用 |
| 二分查找 | O(log n) | 迭代 O(1) | 前提是数据有序 |
| 冒泡 / 选择 / 插入排序 | 通常 O(n²) | 通常 O(1) | 插入排序最好情况可为 O(n) |
| 归并排序 | O(n log n) | O(n) | 稳定,需辅助数组 |
| 快速排序 | 平均 O(n log n),最坏 O(n²) | 平均递归栈 O(log n) | 取决于分区和 pivot |
| BFS / DFS | O(V + E) | O(V) | 图用邻接表表示时 |
排序算法的稳定性和实现细节见排序算法。
拿到一道算法题,可以按下面顺序回答:
n 是数组长度,还是 m × n 的矩阵?示例:滑动窗口通常用两个指针让每个元素最多进出窗口一次,因此时间复杂度为 O(n);如果使用哈希表记录窗口状态,额外空间通常为 O(k),其中 k 是窗口内不同元素数量,最坏可写成 O(n)。
O(n) 次时才是 O(n²);要看真实循环范围。O(1) 通常是平均复杂度,不是所有情况下的严格保证。O(n log n) 不一定比 O(n²) 在小数据上快,复杂度描述的是增长趋势。n 的结果时,输出空间是否计入要明确说明。可以这样回答:
时间复杂度描述算法运行时间随输入规模增长的趋势,空间复杂度描述额外内存的增长趋势。分析时先确定输入规模,再看循环、嵌套、递归和数据结构操作,最后忽略常数与低阶项。空间分析不能遗漏哈希表、辅助数组和递归栈,还要区分最好、平均、最坏和摊销复杂度。