1. 刷题笔记的定位与整体设计思路
1.1 为什么选择 CodeTop 作为刷题主线
LeetCode 题目总量已经超过三千道,如果漫无目的地刷,很容易陷入“刷了忘、忘了刷”的循环。CodeTop 这类高频题单的价值在于,它把有限的时间聚焦到面试中真正反复出现的题目上。我自己的做法是:以 CodeTop 的高频排序为骨架,按“算法思想”而不是“题目编号”来组织笔记。
具体来说,我把整个笔记分成四大块:数据结构类(链表、栈、队列、堆、哈希)、算法思想类(双指针、滑动窗口、二分、回溯、动态规划、贪心)、图论与搜索类(DFS、BFS、拓扑排序、并查集)、数学与位运算类。每一道题在笔记里不是孤立存在的,而是挂在某个思想节点下,这样复习时是“按套路复习”,而不是“按题号复习”。
为什么这样设计?因为面试考察的本质不是“你见过这道题没有”,而是“你能不能识别出这属于哪一类问题,并快速调用对应的模板”。按思想归类,能让你在遇到新题时迅速做模式匹配。我试过纯按题号顺序刷,结果遇到变形题就懵;改成按思想归类后,同类题的迁移能力明显提升。
1.2 笔记的字段结构设计
每道题的笔记我固定用这几个字段,缺一不可:
- 题目编号与名称:方便回查原题。
- 一句话核心:用一句话说清这题到底在考什么,比如“快慢指针找环入口”。
- 思路演进:先写暴力解法,再写优化解法,记录为什么优化、优化掉了什么。
- 代码模板:可复用的代码骨架,不是完整题解,而是关键片段。
- 复杂度:时间与空间,必须写清楚推导过程,不能只写结论。
- 易错点:边界条件、初始化、循环终止条件等。
- 变形题:这题换個问法会怎么考。
这个结构的好处是,复习时只看“一句话核心”和“易错点”就能快速唤醒记忆,不需要重读整段代码。我实测下来,用这套字段整理一百道高频题,复习一轮的时间能从十几个小时压缩到三四个小时。
1.3 刷题节奏与笔记更新频率
我的节奏是:新题与复习题按 1:2 配比。也就是说,每刷一道新题,必须复习两道旧题。很多人刷题效率低,就是因为只往前冲,不回头。CodeTop 前一百题我大概花了一个半月刷完第一轮,但真正形成肌肉记忆是在第二轮和第三轮复习中。
笔记不是一次写完就完事。第一遍写的是“当时的理解”,第二遍复习时会补充“新的感悟”,第三遍会精简掉冗余表述。我习惯用不同颜色标注:黑色是初版,蓝色是复习补充,红色是考前必看。这样笔记本身也在迭代,越到后面越精炼。
提示:不要追求笔记的美观,追求的是“三个月后的自己能不能看懂”。我见过太多人把笔记做得像手账,结果复习时根本找不到重点。
2. 核心数据结构类题目的笔记拆解
2.1 链表题:指针操作的边界艺术
链表题是 CodeTop 里的重头戏,反转链表、环形链表、合并有序链表、K 个一组翻转,几乎每场面试都会碰到。链表题的核心难点不在思路,而在指针操作的边界处理。
以反转链表为例,我的笔记里记录了三个版本:
第一版是迭代法,用 prev、curr、next 三个指针。关键点是循环终止条件是curr != null,而不是curr.next != null。这个细节我踩过坑:如果写成后者,最后一个节点就不会被反转。代码模板如下:
def reverseList(head): prev = None curr = head while curr: nxt = curr.next curr.next = prev prev = curr curr = nxt return prev第二版是递归法,核心是head.next.next = head; head.next = None。递归法的易错点是忘记把head.next置空,导致形成环。我在笔记里特意用红笔标注了这一点。
第三版是“头插法”,适合在 K 个一组翻转中复用。头插法的好处是不需要额外的 next 指针变量,但需要 dummy 节点辅助。
链表题的通用技巧我总结了三条:画图、dummy 节点、快慢指针。画图能避免指针丢失,dummy 节点能统一头节点的处理逻辑,快慢指针能解决找中点、找环、找倒数第 K 个等问题。这三条几乎覆盖了百分之八十的链表题。
2.2 栈与队列:单调栈的思维转换
单调栈是 CodeTop 里容易被低估的一类题。接雨水、柱状图中最大矩形、每日温度,本质上都是单调栈的应用。我的笔记里把单调栈的核心总结为一句话:找每个元素左边或右边第一个比它大或小的元素。
以每日温度为例,暴力解法是 O(n²),单调栈可以优化到 O(n)。关键思路是维护一个递减栈,栈里存的是下标。当遇到比栈顶元素大的温度时,弹出栈顶并计算距离。代码模板:
def dailyTemperatures(T): res = [0] * len(T) stack = [] for i, t in enumerate(T): while stack and T[stack[-1]] < t: idx = stack.pop() res[idx] = i - idx stack.append(i) return res易错点在于:栈里存下标而不是值,因为需要计算距离。另外,循环结束后栈里剩余的元素对应的结果保持为 0,不需要额外处理。
单调队列则是另一类问题,典型的是滑动窗口最大值。我的笔记里把单调队列和单调栈做了对比:栈是后进先出,队列是先进先出;栈通常处理“下一个更大元素”,队列通常处理“窗口内的最值”。这个对比让我在遇到新题时能快速判断该用哪个。
2.3 哈希表:空间换时间的经典权衡
哈希表类题目在 CodeTop 里出现频率极高,两数之和、字母异位词分组、最长连续序列都是典型。哈希表的核心思想是用空间换时间,把查找操作从 O(n) 降到 O(1)。
以最长连续序列为例,暴力解法是排序后遍历,O(n log n)。哈希表解法是把所有数放入集合,然后对每个数判断它是不是序列的起点(即num - 1不在集合中),如果是就往后延伸。这样每个数最多被访问两次,整体 O(n)。
我的笔记里特别标注了一个易错点:一定要判断起点,否则会重复计算。比如序列[1,2,3,4],如果从 2 开始延伸,就会多做无用功。判断起点的条件是num - 1 not in set。
哈希表的另一个常见坑是哈希冲突。虽然刷题时很少需要自己实现哈希函数,但理解冲突处理(链地址法、开放寻址法)对面试追问很有帮助。我在笔记里用一段话解释了 Java 的 HashMap 为什么在链表长度超过 8 时转红黑树,这个知识点在面试中经常被问到。
2.4 堆与优先队列:Top K 问题的标准解法
堆类题目在 CodeTop 里主要集中在 Top K 问题上,比如数组中的第 K 个最大元素、前 K 个高频元素、合并 K 个有序链表。堆的核心价值是在 O(n log k) 时间内维护一个大小为 k 的有序结构。
以第 K 个最大元素为例,有两种思路:一种是用小顶堆维护前 K 大,堆顶就是答案;另一种是快速选择算法,平均 O(n)。我的笔记里把两种方法都记了,并标注了适用场景:如果数据是流式的,用堆;如果数据是静态数组,快速选择更快。
堆的易错点在于 Python 的 heapq 默认是小顶堆,如果要大顶堆,需要存负数。这个技巧我在笔记里写了三遍,因为实在太容易忘。代码模板:
import heapq def findKthLargest(nums, k): heap = [] for num in nums: heapq.heappush(heap, num) if len(heap) > k: heapq.heappop(heap) return heap[0]合并 K 个有序链表是堆的另一个经典应用。思路是把每个链表的头节点放入堆,每次弹出最小节点,然后把它的下一个节点入堆。时间复杂度 O(N log k),N 是总节点数。这个题我在面试中遇到过三次,每次都是用同一套模板。
3. 算法思想类题目的笔记拆解
3.1 双指针与滑动窗口:从暴力到线性的桥梁
双指针和滑动窗口是 CodeTop 里最实用的技巧之一。三数之和、无重复字符的最长子串、盛最多水的容器、最小覆盖子串,都是这类题的代表。
双指针的核心是利用有序性或单调性,减少不必要的枚举。以盛最多水的容器为例,暴力是 O(n²),双指针是 O(n)。关键洞察是:容器的容量由较短的那块板决定,所以移动较长的那块板不可能增加容量,只能移动较短的那块。这个推理过程我在笔记里写得很详细,因为面试时面试官往往追问“为什么这样移动是对的”。
滑动窗口的核心是维护一个满足条件的窗口,并在不满足时收缩左边界。以无重复字符的最长子串为例,用哈希表记录字符最后出现的位置,右指针扩张时如果遇到重复字符,左指针跳到重复字符的下一位。代码模板:
def lengthOfLongestSubstring(s): last = {} left = 0 res = 0 for right, ch in enumerate(s): if ch in last and last[ch] >= left: left = last[ch] + 1 last[ch] = right res = max(res, right - left + 1) return res易错点是last[ch] >= left这个判断。如果只写ch in last,当重复字符在窗口外时会导致左指针回退。这个坑我在第一轮刷题时踩过,后来在笔记里用红笔圈了出来。
滑动窗口的通用模板我总结为四步:右指针扩张、更新状态、判断是否收缩、更新答案。这个模板可以套用到大部分子串、子数组问题上。
3.2 二分查找:边界条件的魔鬼细节
二分查找看似简单,但边界条件极其容易出错。CodeTop 里的二分题包括搜索旋转排序数组、寻找两个正序数组的中位数、在排序数组中查找元素的第一个和最后一个位置。
我的笔记里把二分分为三类:查找目标值、查找左边界、查找右边界。每类的循环条件和返回值都不同。以查找左边界为例:
def lowerBound(nums, target): left, right = 0, len(nums) while left < right: mid = (left + right) // 2 if nums[mid] < target: left = mid + 1 else: right = mid return left关键点是右边界初始为len(nums)而不是len(nums) - 1,循环条件是left < right而不是left <= right。这样返回的left就是第一个大于等于 target 的位置。
搜索旋转排序数组是二分的进阶题。核心思路是:每次二分后,总有一半是有序的,判断 target 是否在有序的那一半,然后缩小范围。易错点在于判断哪一半有序时的条件:nums[left] <= nums[mid]表示左半有序,注意是<=而不是<,因为当left == mid时左半只有一个元素,也是有序的。
我在笔记里写了一句总结:二分的本质是在一个单调的判定函数上找分界点。理解这句话,大部分二分题都能迎刃而解。
3.3 回溯:排列组合问题的通用框架
回溯类题目在 CodeTop 里包括全排列、子集、组合总和、括号生成、单词搜索。回溯的核心是做选择、递归、撤销选择。
以全排列为例,模板如下:
def permute(nums): res = [] def backtrack(path, used): if len(path) == len(nums): res.append(path[:]) return for i in range(len(nums)): if used[i]: continue used[i] = True path.append(nums[i]) backtrack(path, used) path.pop() used[i] = False backtrack([], [False] * len(nums)) return res易错点有三个:一是path[:]深拷贝,否则结果会被后续修改;二是used数组的恢复;三是循环的起始位置。对于组合问题(如组合总和),循环需要从start开始而不是从 0 开始,避免重复组合。
去重是回溯的另一个难点。以子集 II 为例,数组中有重复元素,需要先排序,然后在循环中跳过i > start and nums[i] == nums[i-1]的情况。这个条件我在笔记里画了图解释:同一层中相同的元素只选第一个,不同层可以选相同的。
回溯的时间复杂度通常是 O(n!),空间复杂度 O(n)。虽然复杂度高,但通过剪枝可以大幅减少实际运行时间。剪枝的技巧包括:排序后提前终止、预判剩余元素是否足够等。
3.4 动态规划:状态定义与转移方程
动态规划是 CodeTop 里占比最大的类别,也是最难的一类。我的笔记里把 DP 分为五类:线性 DP、区间 DP、背包 DP、状态压缩 DP、树形 DP。
线性 DP 的代表是最大子数组和、最长递增子序列。以最长递增子序列为例,O(n²) 的解法是dp[i]表示以nums[i]结尾的最长递增子序列长度,转移方程是dp[i] = max(dp[j]) + 1,其中j < i且nums[j] < nums[i]。O(n log n) 的解法是用贪心加二分,维护一个 tails 数组。
背包 DP 的代表是零钱兑换、分割等和子集。以零钱兑换为例,dp[i]表示凑成金额 i 的最少硬币数,转移方程是dp[i] = min(dp[i - coin]) + 1。易错点是初始化dp[0] = 0,其他为无穷大。
区间 DP 的代表是最长回文子序列、戳气球。以最长回文子序列为例,dp[i][j]表示s[i..j]的最长回文子序列长度。转移方程分两种情况:如果s[i] == s[j],则dp[i][j] = dp[i+1][j-1] + 2;否则dp[i][j] = max(dp[i+1][j], dp[i][j-1])。遍历顺序是关键:必须从下到上、从左到右,保证子问题先被计算。
我在笔记里写了一句心得:DP 的难点不在写代码,而在定义状态和推导转移方程。这两步做对了,代码就是翻译。为了训练这两步,我习惯在写代码前先用中文把状态和转移写清楚,再翻译成代码。
4. 图论与搜索类题目的笔记拆解
4.1 DFS 与 BFS:搜索的两把利器
DFS 和 BFS 是图论题的基础。CodeTop 里的岛屿数量、腐烂的橘子、课程表、单词接龙都是这类题。
DFS 的核心是递归加标记。以岛屿数量为例,遍历网格,遇到陆地就计数加一,然后用 DFS 把相连的陆地全部标记为已访问。代码模板:
def numIslands(grid): def dfs(i, j): if i < 0 or i >= len(grid) or j < 0 or j >= len(grid[0]) or grid[i][j] != '1': return grid[i][j] = '0' dfs(i+1, j) dfs(i-1, j) dfs(i, j+1) dfs(i, j-1) count = 0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == '1': count += 1 dfs(i, j) return countBFS 的核心是队列加层序遍历。以腐烂的橘子为例,把所有腐烂的橘子入队,然后逐层扩散,每层时间加一。易错点是初始时可能没有腐烂的橘子,需要特判。
DFS 和 BFS 的选择:如果问题是“是否存在路径”或“遍历所有可能”,用 DFS;如果问题是“最短路径”或“最少步数”,用 BFS。这个判断标准我在笔记里加粗标注了。
4.2 拓扑排序:有向无环图的依赖处理
拓扑排序在 CodeTop 里主要出现在课程表问题中。核心思路是统计入度,把入度为 0 的节点入队,然后逐个弹出并减少相邻节点的入度。
代码模板:
from collections import deque def canFinish(numCourses, prerequisites): graph = [[] for _ in range(numCourses)] indegree = [0] * numCourses for a, b in prerequisites: graph[b].append(a) indegree[a] += 1 queue = deque([i for i in range(numCourses) if indegree[i] == 0]) count = 0 while queue: node = queue.popleft() count += 1 for nxt in graph[node]: indegree[nxt] -= 1 if indegree[nxt] == 0: queue.append(nxt) return count == numCourses易错点是图的构建方向:prerequisites中[a, b]表示修 a 之前必须先修 b,所以边是b -> a。这个方向我在笔记里画了箭头图,避免搞反。
拓扑排序的另一个应用是“外星文字典”,需要根据字母顺序构建图。这类题的难点在于建图,而不是拓扑排序本身。
4.3 并查集:连通性问题的优雅解法
并查集在 CodeTop 里出现在朋友圈、冗余连接、岛屿数量 II 等题中。核心操作是find和union,加上路径压缩和按秩合并,可以做到近似 O(1)。
代码模板:
class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): rx, ry = self.find(x), self.find(y) if rx == ry: return False if self.rank[rx] < self.rank[ry]: rx, ry = ry, rx self.parent[ry] = rx if self.rank[rx] == self.rank[ry]: self.rank[rx] += 1 return True路径压缩是find中的self.parent[x] = self.find(self.parent[x]),按秩合并是union中的 rank 比较。这两个优化缺一不可,否则在极端情况下会退化成链表。
并查集的易错点是find的递归写法可能导致栈溢出,大数据量时建议用迭代写法。我在笔记里两种写法都记了,并标注了适用场景。
5. 常见问题与排查技巧实录
5.1 刷题过程中的典型卡点
刷题时最常遇到的卡点不是“不会做”,而是“做出来了但过不了”。我把这些卡点整理成了一张速查表:
| 卡点现象 | 可能原因 | 排查方法 |
|---|---|---|
| 结果差一个 | 边界条件错误 | 检查循环终止条件、初始化 |
| 超时 | 复杂度太高 | 分析时间复杂度,考虑优化 |
| 死循环 | 指针未移动 | 检查 while 循环内是否有推进 |
| 结果重复 | 去重逻辑缺失 | 排序后跳过相同元素 |
| 栈溢出 | 递归深度过大 | 改迭代或加记忆化 |
| 答案错误但逻辑对 | 变量作用域问题 | 检查闭包中的变量引用 |
这张表我贴在笔记首页,每次卡住时先对照排查,能解决大部分问题。
5.2 面试中的高频追问与应对
面试官在算法题之后通常会追问几个问题,我总结了最常见的三类:
第一类是复杂度分析。不仅要说出 O(n),还要能推导。比如“为什么快速选择平均是 O(n)”,需要解释每次分区后问题规模减半,期望比较次数是 n + n/2 + n/4 + ... = 2n。
第二类是边界条件。比如“如果输入为空怎么办”“如果数组中有重复元素怎么办”。这类问题考察的是代码的健壮性,我在笔记里每道题都标注了边界情况。
第三类是变形题。比如“如果数据是流式的怎么改”“如果内存有限怎么改”。这类问题考察的是知识迁移能力,我在笔记里每道题都写了至少一个变形方向。
5.3 笔记复习的正确姿势
笔记写完不是终点,复习才是。我的复习方法是三遍法:
第一遍是快速浏览,只看“一句话核心”和“易错点”,唤醒记忆。这一遍大概每道题 30 秒。
第二遍是手写代码,不看模板,凭记忆写出来,然后对照笔记检查。这一遍每道题 5 到 10 分钟。
第三遍是口述思路,假装在面试,把思路讲出来。这一遍每道题 2 到 3 分钟。
三遍下来,一道题大概花 10 到 15 分钟,一百道题就是 15 到 25 小时。听起来很多,但分摊到每天一小时,一个月就能完成一轮。我实测下来,经过三轮复习,面试时遇到高频题基本能做到“秒识别、秒出思路”。
注意:复习时不要只看不写。我见过太多人笔记翻得很熟,但一上手就卡壳。算法是手工艺,必须动手。
5.4 工具与环境的经验之谈
刷题工具我用过几种,最后稳定在“在线判题 + 本地 IDE”的组合。在线判题用来验证正确性,本地 IDE 用来调试和写笔记。本地调试的好处是可以打断点、看变量,比在线判题效率高很多。
笔记工具我用的是纯文本 Markdown,配合 Git 做版本管理。好处是可以随时回滚,也能看到自己的思路演变。我不推荐用复杂的笔记软件,因为刷题笔记的核心是“快进快出”,工具越简单越好。
代码模板我单独放在一个文件里,按类别组织。每次刷题前先扫一眼模板,能快速进入状态。这个习惯让我在面试前能快速热身,不至于手生。
6. 从刷题到面试的迁移策略
6.1 如何把笔记转化为面试能力
笔记是静态的,面试是动态的。把笔记转化为面试能力,关键在于模拟真实场景。我的做法是找朋友互相出题,或者用在线白板自己讲给自己听。讲的时候要注意三点:先说思路、再说复杂度、最后写代码。
先说思路能让面试官跟上你的节奏,也能在思路跑偏时及时被纠正。再说复杂度能展示你的分析能力。最后写代码时要注意命名规范和边界处理,这些细节往往决定面试官对你的评价。
我在笔记里每道题都写了一段“面试口述版”,用口语化的方式把思路讲出来。比如最长递增子序列的口述版是:“这题可以用动态规划,dp[i] 表示以 nums[i] 结尾的最长递增子序列长度,转移的时候往前找比它小的元素。不过这样是 O(n²),可以用贪心加二分优化到 O(n log n),维护一个 tails 数组,tails[i] 表示长度为 i+1 的递增子序列的最小结尾元素。”这段话我练了很多遍,面试时能脱口而出。
6.2 高频题的优先级排序
CodeTop 的题目很多,但真正高频的其实就那几十道。我的笔记里把题目分为三档:
- 第一档(必刷):反转链表、两数之和、无重复字符的最长子串、最长递增子序列、岛屿数量、全排列、二分查找、快速排序、归并排序、LRU 缓存。
- 第二档(重点):接雨水、滑动窗口最大值、课程表、单词搜索、零钱兑换、合并 K 个有序链表、搜索旋转排序数组。
- 第三档(补充):各类变形题和低频题。
第一档必须做到“闭眼写”,第二档做到“有思路”,第三档做到“见过”。这个优先级排序让我在时间有限时能聚焦最重要的内容。
6.3 长期维护笔记的习惯
笔记不是一次性的,而是长期资产。我每隔一段时间会回顾笔记,把已经形成肌肉记忆的题目标记为“已掌握”,把反复出错的题目标记为“重点复习”。这样笔记会越来越薄,但越来越精。
我还习惯在笔记里记录“面试真题”,也就是实际面试中遇到的题目。这些题目往往比 CodeTop 上的更有针对性,因为它们是真实考察过的。记录时我会标注公司类型、面试轮次、题目变形,方便后续复盘。
最后分享一个小技巧:把笔记导出成 PDF 或打印出来,考前翻一遍。纸质版的翻阅体验比电子版好,而且能避免被手机干扰。我每次面试前都会花半小时翻一遍纸质笔记,效果比刷手机好得多。
这个内容后续还可以这样扩展:把每道题的“面试口述版”录成音频,通勤时听;或者把笔记做成 Anki 卡片,利用碎片时间复习。工具不重要,重要的是持续迭代和动手实践。