1. Hot 100 不是题单,是核心知识网络的高频采样
我发现一个很有意思的现象:很多人把 Hot 100 当成“新手村”,以为刷完这 100 道题就算入门了。结果真去刷的时候才发现,里面中等题占了大头,困难题也扎堆。它并不是“简单题合集”,而是官方根据访问热度、提交频率、面试讨论热度筛出来的一个高频采样集。换句话说,它不直接给你搭好知识体系,却告诉你哪些知识节点必须优先踩。
为什么我会把 Hot 100 当成核心题库来反复刷,而不是去追求刷三百道新题?主要有两个原因。第一是覆盖面。这 100 道题几乎把面试常考的数据结构和算法都占全了:数组、哈希表、双指针、滑动窗口、链表、栈、堆、二叉树、字典树、图、回溯、动态规划、贪心,甚至还有位运算和原地算法。你不需要在题库里漫无目的地游荡,先把这些高频节点吃透,就足够应付绝大多数算法面试。第二是复用性。很多所谓的新题,本质上是 Hot 100 某道题换了层皮。比如“三数之和”的去重逻辑理解透了,再看到“四数之和”“最接近的三数之和”不会慌张;“无重复字符的最长子串”的滑动窗口模型,可以迁移到“最小覆盖子串”这类更复杂的题目上。核心模型就那么几十个,Hot 100 就是帮你把这些模型集中采样的捷径。
有人可能会问:刷三遍 100 题,和刷 300 道新题,时间成本差不多,为什么非要重复?我列过一张对比表,感受很直观。
| 对比维度 | 刷 300 道新题 | 把 Hot 100 刷三遍 |
|---|---|---|
| 知识覆盖 | 广而浅,容易遗忘 | 高频节点密集,重复记忆 |
| 面试迁移 | 遇到变体很难联想 | 模型化迁移,见题能分类 |
| 时间投入 | 看起来很多,实际分散 | 相对可控,正反馈更强 |
| 心态 | 容易在第 50 题放弃 | 进度感清晰,越刷越稳 |
| 代码熟练度 | 每道题只写一遍 | 重点题能默写,形成肌肉记忆 |
第一遍刷完,你可能觉得很多题目似曾相识,但合上书还是写不出来,这是正常的。第二遍开始,尝试不看题解复现,你会发现速度明显变快。第三遍,你甚至可以压缩成一句话提示词,比如“接雨水:单调栈,弹栈结算凹槽”。到这个状态,才算是真的掌握。所以别急着嫌弃“重复”,刷题这件事,重复不是失败,重复才是记忆的开始。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 我按“算法能力”重组的十组刷题顺序
Hot 100 原本的顺序是热度和编号排序,但学习路径不应该按人气来。如果你从第 1 题刷到第 100 题,很容易在第 10 题附近就被“盛最多水的容器”卡住,然后开始怀疑自己。我后来做了一件事:把 100 道题按底层算法能力拆成十组,再按照认知依赖关系决定先后顺序。
| 组别 | 训练目标 | 我优先刷的代表题 |
|---|---|---|
| 1. 数组 / 哈希表 | 空间换时间,哈希表的灵活使用 | 两数之和、字母异位词分组、最长连续序列 |
| 2. 双指针 / 滑动窗口 | 相向双指针与伸缩窗口的边界控制 | 盛最多水的容器、三数之和、无重复字符的最长子串 |
| 3. 链表 | 虚拟头节点、快慢指针、反转合并 | 反转链表、合并两个有序链表、环形链表、LRU 缓存 |
| 4. 栈 / 堆 | 单调栈和优先级队列的典型场景 | 有效的括号、每日温度、数组中的第 K 个最大元素 |
| 5. 二叉树 | 递归遍历、DFS 自底向上返回信息 | 二叉树的中序遍历、最大深度、翻转二叉树、路径总和 III |
| 6. 回溯 / DFS | 穷举与状态重置 | 全排列、子集、组合总和、括号生成 |
| 7. 动态规划 | 状态定义、转移方程、遍历顺序 | 爬楼梯、打家劫舍、最长递增子序列、零钱兑换、编辑距离 |
| 8. 图 / 岛屿问题 | 网格 DFS/BFS 与 visited 标记 | 岛屿数量、腐烂的橘子、课程表 |
| 9. 贪心 / 区间 | 局部最优与区间合并思维 | 跳跃游戏、买卖股票的最佳时机、合并区间 |
| 10. 技巧与其他 | 位运算、原地算法、Moore 投票 | 只出现一次的数字、颜色分类、多数元素 |
为什么这个顺序值得参考?因为它按“认知依赖”排序,而不是按难度排序。数组和哈希表是几乎所有题目的地基,先打通;双指针和滑动窗口是数组的进阶,但不需要递归,容易建立信心;链表帮你理解引用和虚拟头节点,指针操作很直观;栈和堆是线性结构往复杂结构过渡的桥梁;二叉树开始引入递归,但模式相对固定,适合集中训练;回溯本质是带上撤销操作的递归,放在二叉树之后正合适;动态规划更抽象,但有回溯经验之后,遇到 DP 题能先画递归树,再翻译成状态转移;图 / 岛屿问题本质上就是 DFS/BFS 的网格版,只是多了 visited 标记;贪心和区间题更偏思维,放在后期当调剂;技巧类题目则用来查漏补缺。
实际执行时,不需要每天每类都碰。我第一轮大致是这样安排的:第一周数组 / 哈希 + 双指针,第二周链表 + 栈 / 堆,第三周二叉树,第四周回溯 + 动态规划,第五周图 + 贪心 + 技巧。每周末不做新题,只把本周做错的题统一复写一遍。这样五周左右能完成第一轮,比漫无目的地一天五题要扎实得多。
3. 四类高频题型的通用套路与翻车现场
刷到后面你会发现,Hot 100 的高频题其实可以用少数几个套路覆盖。这里我挑四个最容易翻车的题型,说说它们背后的通用规律。
3.1 滑动窗口:right 负责扩展,left 负责收缩
以“无重复字符的最长子串”为例,这是 Hot 100 里滑动窗口的入门必刷题。很多人第一次写会卡在窗口收缩上。核心思路是:右指针不断向前扩展,把新字符加入窗口;一旦当前字符在窗口里重复了,左指针就不断向右收缩,直到重复消失。窗口始终维持“无重复字符”的状态,每次扩展后记录窗口长度。
python复制def lengthOfLongestSubstring(s: str) -> int:
window = set()
left = 0
ans = 0
for right, ch in enumerate(s):
while ch in window:
window.remove(s[left])
left += 1
window.add(ch)
ans = max(ans, right - left + 1)
return ans
这里最关键的是:为什么用 while 而不是 if?因为左指针可能需要连续移动多次,才能把重复字符移出去。比如窗口里是 "abc",下一个字符是 'c',左指针要从 'a' 一路移到 'b' 之后,才能让窗口重新合法。如果只移动一次,窗口里依然有重复字符,后面记录的长度就可能是错的。另外,答案要在窗口收缩完成之后更新,别在加入字符之前更新,那会记录到非法窗口的长度。
相向双指针是另一个方向。比如“盛最多水的容器”,核心是每次移动较短的那一侧,因为移动较长一侧,容器的高度不会变高,宽度反而缩小,面积一定不会变大。这种单调性分析是双指针题里最常见的推理方式。
3.2 二叉树递归:先想清楚递归函数返回什么
树题是 Hot 100 的大头,但很多人一写递归就开始乱。最常见的错误是:递归函数返回什么?是子树的高度、路径和,还是布尔值?没想清楚就动手,写出来的代码总是改来改去。
以“二叉树中的最大路径和”为例。这题的路径可以从任意节点出发,在任意节点结束,路径上可以经过一个节点并选择它的左右子树各一条边。关键思路是:递归函数返回“当前节点作为路径一端时,能往上贡献的最大值”,而全局答案在递归过程中不断更新。
python复制def maxPathSum(root):
ans = float("-inf")
def dfs(node):
nonlocal ans
if not node:
return 0
left = max(dfs(node.left), 0)
right = max(dfs(node.right), 0)
ans = max(ans, left + node.val + right)
return node.val + max(left, right)
dfs(root)
return ans
两个容易漏掉的点:第一,左右子树的负贡献要截断成 0,因为路径可以选择不走某一边,强行加入负数只会让结果更差。第二,返回值只能取 node.val + max(left, right),不能把左右两边都带上去。因为向上走时,路径不能分叉,只能选择其中一边继续延伸。很多新手在这里把代码写成 return node.val + left + right,结果整个子树都被重复计算了。
还有“路径总和 III”,需要双重递归:外层递归遍历每个节点,内层递归统计以该节点为起点是否存在路径和等于目标值。这个题容易超时,要注意剪枝,但更常见的问题是把路径理解成“必须从根节点开始”,于是漏掉了大量合法情况。
3.3 动态规划:先写递归,再翻译成递推
动态规划在 Hot 100 里占了相当大的比例,也是最考验抽象能力的题型。我的经验是:不要一上来就硬写 DP 数组,先尝试写一个暴力递归,再把递归过程翻译成递推,这样状态定义和转移方程都不容易出错。
以“零钱兑换”为例。题目要求用最少的硬币数量凑出指定金额,每种硬币可以用无限次。递归思路是:f(amount) 表示凑出 amount 所需的最少硬币数,然后遍历所有硬币,取 f(amount - coin) + 1 的最小值。翻译成动态规划后:
python复制def coinChange(coins, amount):
dp = [float("inf")] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for c in coins:
if c <= i:
dp[i] = min(dp[i], dp[i - c] + 1)
return dp[amount] if dp[amount] != float("inf") else -1
这个题常见的翻车点有两个。一是初始化:dp[0] = 0,其他都初始化为无穷大,否则 min 的结果永远是 0。二是遍历顺序:这里外层是金额,内层是硬币,因为我们只关心最少数量,不关心组合顺序。但如果题目改成“凑成目标金额的组合数”,遍历顺序就变了,不能直接套。
“编辑距离”更经典。状态 dp[i][j] 表示 word1 的前 i 个字符转换成 word2 的前 j 个字符需要的最少操作数。初始化时要注意和空串比较:dp[i][0] = i,因为要删除 i 次;dp[0][j] = j,因为要插入 j 次。转移时,如果 word1[i-1] == word2[j-1],直接继承 dp[i-1][j-1];否则取删除、插入、替换三种操作的最小值加一。这里最容易错的是下标对应,字符串的第 i 个字符在代码里要写成 word1[i-1],因为 dp 下标从 0 开始,比字符下标多 1。
3.4 回溯:状态重置是灵魂
回溯题在 Hot 100 里也很常见。很多人能理解递归,但容易漏掉“撤销选择”这一步。以“全排列”为例,正确答案长这样:
python复制def permute(nums):
res = []
path = []
used = [False] * len(nums)
def dfs():
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])
dfs()
path.pop()
used[i] = False
dfs()
return res
这里有几个细节必须养成肌肉记忆。第一,res.append(path[:]) 而不是 res.append(path),因为后面 path 会被改变,如果直接 append,最后 res 里全是同一个空列表。第二,递归之后一定要做状态重置:path.pop() 和 used[i] = False 缺一不可。回溯的本质是“尝试 - 递归 - 撤销”,撤销是把状态恢复到进入递归之前的样子,好让下一次循环继续尝试。第三,used 判断要用下标 i,不要用数组里的值去判断,因为可能会出现重复值,用值判断会直接出 bug。
“组合总和”这类题还多一个剪枝问题。如果先对数组排序,当当前元素已经让目标值变成负数,后面的元素更大,也不用继续尝试了,可以直接 break。这个剪枝很自然,但很多人会忘记排序,导致无法使用这个优化。
4. 复盘模板:从 AC 到能讲清楚的四个问题
刷题最怕的不是不会,而是“AC 之后再也不看”。我见过很多面试者,做过的题能写出来,但一问为什么这样解,立刻卡壳。这说明他记住的是代码,不是模型。为了逼自己从“能 AC”升级到“能讲清楚”,我给自己定了一个复盘模板,就四个问题。
第一,这道题考的是哪一个核心模型?是哈希表、双指针、滑动窗口、二叉树递归、回溯,还是动态规划?这个答案应该能在 5 秒内说出口。如果说不出来,说明你还没形成题目分类感。第二,暴力解长什么样?优化点又在哪里?是空间换时间,还是利用了单调性,还是去掉了重复计算?能说清楚暴力解到最优解的变化,才算理解这道题为什么这么解。第三,有哪些边界条件?空输入、单元素输入、全负数、重复元素、溢出,都是高频边界陷阱。第四,如果 30 秒后要给别人口述,你会用什么关键词提示?这一步是为了压缩信息,方便后续快速复习。
以几道 Hot 100 里的代表题为例,我的复盘记录长这样:
| 题目 | 核心模型 | 暴力解 | 优化点 | 边界条件 | 提示词 |
|---|---|---|---|---|---|
| 两数之和 | 哈希表 | 两层循环 O(n^2) | 空间换时间,边查边存 | 不能重复用同一个下标 | 边查边放 |
| 最大子数组和 | 动态规划 / 贪心 | 枚举所有子数组 | 滚动前缀,负数即弃 | 全负数时保留最大负数 | 负了就扔 |
| 接雨水 | 单调栈 / 前后缀最大 | 按列枚举 O(n^2) | 单调栈维护凹槽,弹栈结算 | 数组长度小于 3 接不到水 | 找凹槽,弹栈结算 |
这样一张表,刷第二轮的时候能帮你省很多时间。比如看到“最大子数组和”,你不需要重新把代码完整推导一遍,先想“负了就扔”这四个字,如果立刻能接上“维护当前和,当前和为负数时重置为当前元素”,这道题就算复习通过了。如果接不上,再打开代码看一遍。
我还习惯给错题打标签。标签结构是:分类 / 题目 / 第一次的错误 / 正确模型 / 提示词 / 下次复习时间。这样第三轮的时候,我只需要按标签筛出“动态规划 + 卡壳”的题,就能精准复习自己的薄弱点,而不是从头再刷一遍所有题目。
复盘最有效的方式其实是讲给别人听。哪怕没有真实听众,也可以自己对着手机录音讲思路。讲的过程会把很多模糊的地方暴露出来:为什么这里要排序?为什么去重条件是这个?为什么状态转移是取最小而不是取最大?能把这些“为什么”回答清楚,一道题才算真正消化。
5. 三轮刷法:从“见过”到“肌肉记忆”再到“压缩复习”
确定了题单和复盘方法之后,剩下的就是执行节奏。我不推荐“一遍过,把 100 道题全部做一遍就收工”的做法,而是建议分三轮推进,每一轮目标不一样。
第一轮的目标是建立知识地图。按第 2 章的十组顺序推进,每天 2-3 道题,不要贪多。遇到不会的题太正常了,我的动作是:先硬想 15 分钟,想不出来就看题解。看完题解先不急着关掉,用手挡住代码,尝试自己写一遍。如果写不出来,就再看题解再写,直到能独立完成为止。这个过程很慢,可能一道题要磨一个小时,但比“看完就关”强一百倍。第一轮结束时,对每道题至少要有分类标签和模糊印象,能独立 AC 一半以上就算达标。
第二轮的目标是独立解题。可以打乱顺序,或者按专题再过一遍。这一轮只做第一轮标记为 B(有思路但卡壳)和 C(完全没思路)的题,以及少数即使会做也值得反复写的经典题,比如 LRU 缓存、编辑距离、接雨水。关键动作是:先口述思路,再动手写。看到“合并 K 个升序链表”,你要能说出“用小顶堆维护当前每个链表的头节点,每次弹出最小节点,然后把它的 next 放进堆”;看到“岛屿数量”,要能说出“遍历每个格子,遇到 1 就 DFS,同时把相邻的 1 全部改成 0,计数加一”。能口述出来,代码往往是水到渠成。
第三轮的目标是压缩复习。这时不要沉浸在代码里,而是只看题目,说思路。我给每道题都准备了一个不超过 10 个字的提示词,比如“反转链表:三指针”“环形链表:快慢指针”“有效括号:栈”“最长有效括号:栈或 DP”。面试前两周,我每天随机过 20-30 道题,看到题目标题,如果能在 60 秒内说出提示词和大致过程,就算通过;想不起来的题目标记出来,第二天集中复习。
| 轮次 | 建议周期 | 每天量 | 通过标准 |
|---|---|---|---|
| 第一轮 | 3-4 周 | 2-3 题 | 能独立 AC 一半以上 |
| 第二轮 | 2-3 周 | 4-5 题 | 不看题解恢复 80% 思路 |
| 第三轮 | 考前 2 周 | 20-30 题口述 | 60 秒内说出核心思路 |
三轮下来,你会发现一个很微妙的变化:你不再靠记忆背题,而是看到题目就会自动归类,“这类问题应该往哪个方向想”会在脑子里马上弹出来。我自己刷到后期的明显体会是,Hot 100 不是刷完就扔的题单,它更像一个压缩包,每次复习都能解压出新的理解。状态不好的时候,我也不会硬开新题,而是把错题本翻出来,只看提示词回忆思路。保持手感比追求数量重要得多。
