FAQ?
如何分析一道算法题的时间复杂度与空间复杂度?
时间复杂度用大 O 表示法描述算法运行时间随输入规模 n 的增长量级,常关注最坏情况。分析方法:
- 看循环嵌套层数:单层循环 O(n),双层嵌套 O(n²),但要看是否每次减半——
for (i = 1; i < n; i *= 2)是 O(log n) 而非 O(n)。 - 看递归深度 × 每层工作量:递归树每个节点的工作量乘节点数。如归并:每层 O(n),共 log n 层 → O(n log n)。
- 主定理(Master Theorem):
T(n) = a * T(n/b) + f(n),分治类标准工具。 - 均摊分析:如动态数组 push 平均 O(1),但扩容时 O(n) 摊到 n 次操作上仍为均摊 O(1)。
空间复杂度看额外内存:
- 原地操作:O(1)
- 递归:递归栈深度 O(h)
- DP:状态数组大小 O(n) 或 O(n²),可滚动数组优化
- BFS 队列:最坏 O(宽),回溯递归:最坏 O(深)
| 复杂度 | 含义 | 典型算法 |
|---|---|---|
| O(1) | 常数 | 哈希查找、位运算 |
| O(log n) | 对数 | 二分查找、平衡树操作 |
| O(n) | 线性 | 单次遍历、双指针 |
| O(n log n) | 线性对数 | 排序、归并、分治 |
| O(n²) | 平方 | 朴素 DP、双重循环 |
| O(2^n) | 指数 | 子集枚举、朴素回溯 |
| O(n!) | 阶乘 | 全排列、N 皇后 |
面试经验:n <= 10 大概率 O(n!);n <= 20 是 O(2^n);n <= 100 是 O(n³);n <= 1000 是 O(n²);n <= 10^5 是 O(n log n);n >= 10^9 要 O(log n) 或 O(1)。
算法的稳定性是什么意思,为什么重要?
稳定性指排序后两个相等元素的相对顺序是否保持不变。若排序前 A 在 B 前,且 A == B,排序后 A 仍在 B 前,则称该排序稳定。
稳定的排序:冒泡、插入、归并、计数、基数、TimSort。 不稳定的排序:选择、快排、堆排、希尔。
重要场景:多关键字排序。比如先按次要字段排序(稳定),再按主字段排序,主字段相同时次要字段顺序自然保留。例如:员工按工资排序,工资相同时保持工龄顺序——只需先按工龄排(稳定),再按工资排(稳定)。
为什么快排/堆排不稳定? 因为它们通过交换不相邻元素实现排序,可能跨越相等元素。如选择排序 [5a, 5b, 2] → 选最小 2 与 5a 交换 → [2, 5b, 5a],5a/5b 顺序被破坏。
ES2019 后:JS 的 Array.prototype.sort 规范 明确要求稳定,所有主流浏览器现已实现稳定排序(V8 用 TimSort)。所以业务代码中按多字段排序可放心链式 sort。
递归和迭代如何转换?什么时候必须用递归?
递归 → 迭代有两条路径:
- 直接改循环:尾递归可以直接改写成 while。如阶乘
fact(n, acc=1)→while (n > 0) { acc *= n; n--; }。斐波那契用两个变量滚动。 - 显式栈模拟:任意递归都能用栈模拟——把"函数调用栈"换成"显式栈"。DFS 的递归版改迭代版就是经典例子:把节点压栈,pop 处理,子节点压栈。
// 递归版前序遍历
function preorderRec(root: TreeNode | null): number[] {
if (!root) return [];
return [root.val, ...preorderRec(root.left), ...preorderRec(root.right)];
}
// 迭代版(显式栈)
function preorderIter(root: TreeNode | null): number[] {
if (!root) return [];
const res: number[] = [];
const stack: TreeNode[] = [root];
while (stack.length) {
const node = stack.pop()!;
res.push(node.val);
if (node.right) stack.push(node.right); // 先压右,后压左
if (node.left) stack.push(node.left);
}
return res;
}
什么时候必须用递归(或显式栈)?
- 问题天然是递归结构:树的遍历、分治、回溯、表达式求值、文件系统遍历。
- 需要保存"上下文"的:回溯的"