📑 本页目录(点开跳转)
附录A · 速查
⏱ 31 分钟 | ⭐ 临场想不起来时翻这一页:不变量一句话、模板一小段、写错了长什么样
📌 这一页是查的,不是读的。每一条都指回本板块的某一章。
🎯 一句话
这一页不讲怎么想出来,只把已经想出来的东西排好。 正文八章负责让你懂,这一页负责让你在白板前十秒钟内找回那句关键的不变量。
⚠️ 里面没有一行新代码 —— 全部从正文抄回,改了就对不上了。真要练,回到对应那一章把整段跑一遍。
🧭 一、先认出题型:数据形状 → 你真正要解决的问题
⭐ 00 章那条判据是这一页的骨架:一题先翻译成数据形状 + 一个变化动作 + 一个不变量,代码才会自己长出来。
| 类别 | 你真正要解决的问题 | 本板块代表题 | 章 |
|---|---|---|---|
| 链表 | 节点没有下标,怎样不丢路 | 反转、合并、环入口 | 01 |
| 树 | 一层分成多条路,怎样把结果带回来 | 深度、层序、路径和、最近公共祖先 | 02 |
| 栈与队列 | 只能从一端拿时,怎样记住「最近」 | 两栈队列、min 栈、滑动窗口最大值 | 03 |
| 搜索与排序 | 不枚举全部,怎样砍掉不可能的半边 | 二分边界、旋转数组、最小 k 个数 | 04 |
| 动态规划 | 今天的最优,怎样由昨天的状态推出 | 最大子数组和、跳台阶、正则匹配 | 05 |
| 回溯 | 有分叉时,怎样试完一条再干净地回来 | 矩阵路径、机器人范围、排列 | 06 |
| 位运算与模拟 | 规则很机械,怎样逐步翻译而不漏边界 | 异或分组、二进制加法、螺旋矩阵 | 07 |
| 字符串 | 输入很脏,怎样一格一格地吃进去 | atoi、最长无重复子串、字符流 | 08 |
📋 「我现在卡在哪」→ 直接跳哪一章:
| 你的症状 | 去哪 |
|---|---|
| 指针、链表一看就晕 | 01 链表 |
| 递归一写就炸 | 02 树 |
| 想学面试常用模板 | 03 栈队列与窗口 → 04 二分排序与堆 |
| 想练「怎么想出来」 | 05 动态规划 → 06 回溯与矩阵 |
| 想补零碎高频题 | 07 位运算与模拟 → 08 字符串与输入 |
🧩 二、⭐ 不变量卡:每题一句,忘了就看这一句
⭐ 这是全页最该扫的一节。 每一条都是正文里那句「循环里始终为真的事」。
| 题 | 一句话不变量 | 章 |
|---|---|---|
| 反转链表 | 每轮结束后,prev 指向已反转的前缀,cur 指向尚未处理的后缀 |
01 |
| 合并两个有序链表 | tail 后面永远是已排好序的结果;a、b 各指向还没被拿走的最小节点 |
01 |
| 链表中环的入口 | 快慢相遇 ⇒ 有环;一个回头,两人同速再见就是入口 | 01 |
| 二叉树深度 | depth(node) 永远返回「以 node 为根的子树有多高」,父节点不必知道孩子内部怎么数 |
02 |
| 层序打印 | 队列里排着的就是「先来的先处理」 | 02 |
| 路径和 | path 始终是当前这条根到节点的路:进节点 append,离开节点 pop |
02 |
| 最近公共祖先 | 递归交回「在这棵子树里找到的目标节点,或已经确定的公共祖先」 | 02 |
| 两个栈实现队列 | outbox 顶部永远是当前最早进入、还没出去的元素 |
03 |
| min 栈 | mins 的每一层记「走到这一层为止的最小值」 |
03 |
| 滑动窗口最大值 | 队列里只留可能成为最大值的人;⭐ 存的是下标不是值 | 03 |
lower_bound |
答案永远在半开区间 [left, right) 里;right 是「答案可能到这里为止」的边界 |
04 |
| 旋转数组最小值 | 中点和右端比:中点更大 ⇒ 最小值在右边;否则在左边或就是中点 | 04 |
| 最小的 k 个数 | 只维护 k 个候选,不必把全部排好 | 04 |
| 连续子数组最大和 | best_ending_here 是「必须以当前位置结尾」的最大和,它不等于全局答案 |
05 |
| 跳台阶 | 到第 n 阶,最后一步不是从 n−1 来就是从 n−2 来 | 05 |
正则匹配(. 和 *) |
* 有两条路:跳过「字符+星号」,或吃一个字符后继续留在原模式 |
05 |
| 矩阵中的路径 | 一个格子在当前路径中不能重复使用:进来标记、离开还原 | 06 |
| 机器人的运动范围 | 已经访问过的格子不用重算;条件是坐标位数之和不超过阈值 | 06 |
| 字符串的排列 | 每一层决定当前位置放谁;同一层遇到重复字符时跳过 | 06 |
| 只出现一次的两个数字 | 全部异或后只剩两个目标的异或;mixed & -mixed 取最低位的 1,两个目标在这一位必不同 |
07 |
| 不用加减乘除做加法 | 异或是不进位的和,& 左移一位是进位;重复到没有进位 |
07 |
| 顺时针打印矩阵 | 不转方向,每绕一圈收缩上、右、下、左四条边;每走一条前确认矩形还没塌 | 07 |
| atoi | 四个阶段固定:跳空格 → 读正负号 → 连续读数字 → 处理溢出;任一步不符合规则就停止,不「尽量猜」 | 08 |
| 最长无重复子串 | 左边界只向右:遇到重复就跳到它上次出现位置的后一格 | 08 |
| 字符流第一个不重复 | 计数负责判断重复,队列负责保留先后;队首重复了就淘汰 | 08 |
🧯 三、⭐ 写错了长什么样:症状 → 它到底在说什么
用法:跑出错误答案或异常之后,在这一节找你看到的那句话。 ⚠️ 这类题最难的地方是多数错误不报异常,只是答案不对 —— 所以下面前半截是「症状」,后半截才是异常。
链表反转后只剩一两个节点,或者结果里出现 None
⭐ 先写了 cur.next = prev,再回头去找原来的 cur.next。 那时后路已经被覆盖,尚未处理的整个后缀就再也找不到了。
→ 顺序永远是:nxt = cur.next 先保住后路 → cur.next = prev 再掉头 → 两队伍一起向前。见 01 章题 1。
合并链表时要写「第一个节点」的特殊分支,越写越乱
→ 用 dummy 假头。⭐ 它存在的唯一理由:让第一个节点和后面的节点一样接,不必写「第一次特殊处理」。见 01 章题 2。
💀 树的路径题:答案条数对,但内容全是空列表
⭐ answer.append(path) 写成了没有 [:] 的版本。 所有答案都指向同一个还会继续变化的列表,回溯弹完之后它们一起变空。
→ 存答案必须 answer.append(path[:])。见 02 章题 3。
树的路径题:右子树的答案里混进了左边的节点
离开一个节点时没有 path.pop()。同一个 path 被左右子树共用,不复原现场,右子树会带着一段不属于它的旧路径继续走。见 02 章题 3。
滑动窗口最大值:窗口滑出去了,答案还是旧的那个最大值
⭐ 队列里存的是值而不是下标。 存下标不是细节 —— 只有知道下标,才判断得出队首是否已经离开窗口(queue[0] <= right - k)。见 03 章题 3。
IndexError: queue is empty(两个栈实现队列)
倒完 inbox 之后 outbox 仍然是空的 —— 队列本来就没有元素。⭐ 注意倒的时机:只有 outbox 空了才整批倒,中途倒会把顺序搅乱。见 03 章题 1。
二分:数组里没有目标时结果不对,或者目标比所有元素都大时越界
⭐ 右边界写成了 len(nums) - 1。 lower_bound 用的是半开区间 [left, right),right = len(nums) 才表达得出「目标比所有元素都大」时的插入位置,也让空数组和边界判断统一。见 04 章题 1。
ValueError: k out of range(最小的 k 个数)
k 不在 0 <= k <= len(nums) 里。⚠️ 这是故意加的边界检查,不是 bug —— 面试里被追问边界时,它就是你的答案。见 04 章题 3。
最大子数组和:只维护了一个变量,答案偏小
⭐ 只记全局最大和,丢掉了「以当前点结尾」的那个状态。 下一轮需要知道前缀能不能接上,而全局最大和可能结束在很早的位置,不能直接接。见 05 章题 1。
递归的正则匹配跑得非常慢
同一批子问题被反复算。⭐ 看见「很多子问题重复算」先考虑记忆化,它就是从递归过渡到动态规划的桥。见 05 章题 3。
矩阵路径:某条真实存在的路径找不到
⭐ 走完一个分支后没把 board[r][c] 恢复回去。 这个格子只是当前分支不能再用;别的起点或兄弟分支仍然应该可以用它。见 06 章题 1。
排列结果里有重复项
同一层里同一个字符被试了不止一次。→ 每层用一个 used 集合,同一层相同字符只尝试一次。见 06 章题 3。
螺旋打印:单行或单列的矩阵,某些元素被打印了两次
⭐ 走下边和左边之前少了那两次边界判断。 只有一行时上边已经把元素拿完了,只有一列时右边已经拿完了。见 07 章题 3。
二进制加法在负数上死循环 / 结果离谱
⚠️ add_without_plus 那段对非负整数最直观。 Python 的负数是无限位语义,进位可以一直左移下去。面试若要求处理负数,要先约定 32 位掩码。见 07 章题 2。
⭐ 反面在 NumPy · 08 整数 dtype 的真相:一进 NumPy 整数就是定宽的,越界不报错、直接回绕,而且没有任何信号。
atoi:'words and 9' 返回了 9
⚠️ 你在「读到非法字符」之后还继续找数字了。⭐ 四个阶段是顺序的,任何一步不符合规则就停止,不是「尽量猜」。正确结果是 0。见 08 章题 1。
最长无重复子串:结果比正确答案小
左边界回退了。⭐ 右边界已经向右扩展;回退左边界只会把已知会冲突的字符重新放回窗口,既无帮助又会重复计算。条件要写成 last_seen[char] >= left。见 08 章题 2。
📦 四、这套题用到的标准库,一张表
⭐ 本板块只用了三个 collections / heapq 的东西,每个都对应一个明确的需求:
| 用到的 | 干什么 | 出现在 |
|---|---|---|
collections.deque |
两端 O(1):BFS 的队列、滑动窗口的单调队列、字符流的先后 | 02 层序 · 03 窗口 · 08 字符流 |
collections.Counter |
计数,用来判断「重复了没有」 | 08 字符流 |
heapq.nsmallest |
只要前 k 个最小元素,不必全排 | 04 最小的 k 个数 |
functools.lru_cache |
记忆化,把重复子问题的递归变成 DP | 05 正则匹配 |
⚠️ 面试若要求手写「最小的 k 个数」,用大小为 k 的最大堆;但先把「只维护 k 个候选」这个思想弄懂,容器是次要的。
⭐ deque 为什么在这些题里非它不可(list.insert(0, x) 是 O(n)、deque.appendleft 是 O(1),实跑差 299~1005 倍),在 Python 会咬你的地方 · 09 容器、哈希与顺序。
🧪 五、每题的小闭环,和「做完了」的标准
⭐ 00 章定的四步,卡住时按它走:
- 先用 3–6 个元素手画一次,不急着敲代码。
- 用一句话写下循环里「始终为真」的事 —— 那就是不变量(第二节整节都是它)。
- 把代码复制运行,至少换一个边界输入:空、一个元素、重复、极值。
- 卡 10 分钟时先看「关键不变量」,不要直接滚到完整代码。
⭐ 完成标准不是 AC 过一次,而是第二天遮住代码后还能说出:指针 / 下标各代表谁、每轮为什么不会漏。
📋 每类题该补的边界输入:
| 类别 | 至少再试这几种 |
|---|---|
| 链表 | 空链表、只有一个节点、没有环 |
| 树 | 空树、只有左子树的斜树、目标不在树里 |
| 栈队列 | 空时取值、k 等于数组长度 |
| 二分 | 目标不存在、目标比所有元素都大、数组只有一个元素 |
| DP | n = 0、全负数数组 |
| 回溯 | 目标字符串比矩阵还长、阈值为 0 |
| 位运算 | 负数、0 |
| 字符串 | 空串、全是同一个字符、开头就是非法字符 |
🔗 这一页连到哪里
| 相关的地方 | 为什么 |
|---|---|
| 00 · 怎么刷代码题 | 第一节那张题目地图和第五节那四步的完整版 |
| Python 会咬你的地方 · 00 | ⭐ 这里的代码全是 Python。被「改了 b 结果 a 也变了」「默认参数记住了上次的值」咬过的话,那一套讲的就是它们 |
| Python 会咬你的地方 · 09 · 容器、哈希与顺序 | 第四节那三个容器各自的复杂度和保不保序:dict 从 3.7 起保证插入序,set 从来不保证 |
| 机器学习与深度学习基础 · 附录C · 手撕代码速查 | ⭐ 另一类白板题:这里练链表 / 树 / DP,那里是「用纯 numpy 写一个多头注意力」。算法岗两类都会被问 |
| 不靠数据的 AI · 05 · 状态空间与搜索框架 | ⭐ 「谁应该先出来」在那边是一个统一骨架:BFS、DFS、迭代加深、一致代价、A* 只差「待展开集合用什么容器」 |
| 不靠数据的 AI · 14 · 回溯与约束传播 | 同一个回溯骨架往上加两件本板块没有的东西:前向检查、弧相容 |
| 强化学习基础 · 04 · 动态规划 | ⚠️ 同名不同物,但共用那句「今天的最优由昨天的状态推出来」 |
| NumPy 与向量化思维 · 08 · 整数 dtype 的真相 | 第三节最后那条的反面:定宽整数越界静默回绕 |
⭐ 这一页最该带走的一句:题目换名字没关系;数据形状和不变量没换,解法就还在。
下一节 👉 回到板块索引