看增长趋势,不看机器速度
复杂度描述输入规模变大后,算法执行次数如何增长。写题时先数循环层数,再观察有没有二分、排序、递归分治或剪枝。
- O(1):常数操作,例如访问数组下标。
- O(log n):每次把范围缩小一半,例如二分查找。
- O(n):每个元素看一次,例如遍历数组。
- O(n log n):常见于排序、分治。
- O(n²):两层循环枚举 pair。
复杂度描述输入规模变大后,算法执行次数如何增长。写题时先数循环层数,再观察有没有二分、排序、递归分治或剪枝。
空间复杂度不只看输入数组本身,更关注你为了计算额外创建了多少结构。哈希表、辅助数组、递归栈、动态规划表都要算。
很多代码不是思路错,而是边界没覆盖。提交前至少手动测空输入、单元素、重复值、负数、最大值、找不到目标、刚好在开头或结尾。
| 题目特征 | 优先考虑 | 判断理由 |
|---|---|---|
| 有序数组、查找位置、最小可行值 | 二分查找 | 有单调性,可以不断缩小答案范围。 |
| 连续子数组、最长/最短、和或字符频次 | 滑动窗口 | 窗口左右边界移动,每个元素进出窗口一次。 |
| 两数之和、去重、出现次数 | 哈希表 | 用空间换时间,把查找从 O(n) 降到接近 O(1)。 |
| 括号匹配、最近未匹配、撤销操作 | 栈 | 后进先出,天然适合处理最近状态。 |
| 层序遍历、最短步数、扩散过程 | 队列/BFS | 按层推进,第一次到达通常就是最短。 |
| 所有方案、排列组合、路径选择 | 回溯 | 需要试探、撤销、继续尝试下一种选择。 |
| 最优值、重复子问题、当前依赖过去 | 动态规划 | 定义状态,写转移方程,按依赖顺序计算。 |
哈希表适合快速判断“有没有出现过”“出现了几次”“某个值对应什么”。它的典型写法是边遍历边记录,或者先统计再查询。
前缀和用来快速求区间和。`prefix[i]` 表示前 i 个数的和,区间 `[l,r]` 的和等于 `prefix[r+1] - prefix[l]`。
双指针的本质是用两个位置共同表示当前状态。常见有左右夹逼、快慢指针、读写指针三类。
滑动窗口适合连续区间问题。右指针扩张窗口,左指针在条件不满足时收缩窗口,窗口内维护频次、和、最大值等信息。
二分不只用于“找某个数”,还常用于“找第一个满足条件的位置”或“答案最小是多少”。关键是判断条件必须具有单调性。
栈是后进先出。只要题目里出现“最近的一个”“匹配”“撤销”“单调关系”,就可以想栈。
队列是先进先出。BFS、层序遍历、任务调度、缓存淘汰都常用队列。
| 排序 | 复杂度 | 适用场景 | 注意点 |
|---|---|---|---|
| 冒泡排序 | O(n²) | 教学、理解交换 | 大数据会超时。 |
| 选择排序 | O(n²) | 简单实现 | 每轮找最小值放前面。 |
| 插入排序 | O(n²) | 数据基本有序 | 小规模时很实用。 |
| 归并排序 | O(n log n) | 稳定排序、逆序对 | 需要辅助数组。 |
| 快速排序 | 平均 O(n log n) | 通用排序 | 极端情况下可能退化。 |
| 堆排序 | O(n log n) | Top K、优先队列 | 理解堆顶含义。 |
递归就是函数调用自己。写递归时不要试图在脑中展开所有层,先明确函数含义,再写终止条件和递推关系。
回溯用于枚举所有方案。基本动作是选择、递归、撤销选择。组合、排列、子集、棋盘路径都属于典型题。
动态规划适合有重复子问题和最优子结构的题。不要一上来写代码,先把状态、转移、初始化、遍历顺序讲清楚。
贪心每一步选择当前看起来最优的方案。它需要证明局部最优可以推出全局最优,否则很容易错。
堆适合动态维护最大值或最小值。Top K、合并有序链表、任务调度、数据流中位数都常用堆。
树题重点是遍历。前序适合复制树、序列化;中序适合二叉搜索树;后序适合自底向上计算高度、直径、平衡性。
BST 的特点是左子树小于根,右子树大于根。中序遍历会得到有序序列。
图由点和边组成。写图题第一步是建图,第二步是选择遍历方式。无权最短路常用 BFS,有权最短路常用 Dijkstra。
并查集用于维护集合合并关系。常见于连通块、朋友圈、岛屿合并、判断是否成环。
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
function windowTemplate(arr) {
let left = 0;
let answer = 0;
const count = new Map();
for (let right = 0; right < arr.length; right++) {
const x = arr[right];
count.set(x, (count.get(x) || 0) + 1);
while (/* 窗口不合法 */ false) {
const y = arr[left];
count.set(y, count.get(y) - 1);
left++;
}
answer = Math.max(answer, right - left + 1);
}
return answer;
}
function backtrackTemplate(nums) {
const ans = [];
const path = [];
function dfs(start) {
ans.push([...path]);
for (let i = start; i < nums.length; i++) {
path.push(nums[i]);
dfs(i + 1);
path.pop();
}
}
dfs(0);
return ans;
}
function fib(n) {
if (n < 2) return n;
let prev = 0;
let curr = 1;
for (let i = 2; i <= n; i++) {
const next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
function bfs(start, graph) {
const queue = [start];
const visited = new Set([start]);
while (queue.length) {
const node = queue.shift();
for (const next of graph[node] || []) {
if (visited.has(next)) continue;
visited.add(next);
queue.push(next);
}
}
return visited.size;
}