
刷题这件事只要经历过技术面试的人都懂。HOT100这100道题在LeetCode上是被标记为“高频”的那批题也是很多人在准备面试时的默认起点。说实话市面上刷题资源多到能把人淹死但真正值得反复研究的往往就是这100道题覆盖的考点和套路。我自己的经历是第一遍乱刷见到什么做什么效率极低后来老老实实按HOT100分类打了一遍配合自己的刷题记录表格差不多三个月时间从看见二叉树就头疼到后来能在白板上直接写层序遍历这个题库的作用是实打实的。这篇东西我写了很久把我从规划到实操、再到复盘整理的完整过程都捋了一遍。里面包含了我踩过的坑、用着顺手的模板代码以及一套我至今还在用的刷题记录方法。如果你想用Python刷HOT100那这篇文章应该能帮你少走不少弯路。我会直接按自己的经验讲不绕弯子。1. 为什么是HOT100这100题到底能解决什么问题1.1 说句公道话HOT100不是万能的很多人把HOT100当成“刷完就能进大厂”的万能钥匙这个预期得先矫正一下。它本质上是一个“高频考点样本库”是很多人用实际面试经验投票投出来的题集覆盖了面试里最常出现的那些基础算法和数据结构。但面试不只看你会不会做题还要看你的沟通能力、代码规范、边界条件考虑得全不全。HOT100能帮你解决的是“算法题这一关”而不是全部环节。不过话说回来算法题这一关确实是很多人的生死关尤其对于校招和社招转岗的人。HOT100的价值在于它把“面试中最高频的算法模型”压缩成了一个相对小规模的集合。我在刷的时候明显感觉到题目是有规律可循的比如哈希表配数组、双指针配排序、二叉树配递归这些组合反复出现。一旦把HOT100里的核心模式吃透遇到没刷过的新题你也至少能判断出它到底在考哪个模型不至于完全懵。1.2 100题背后的覆盖面高频考点一网打尽HOT100的选题分布其实很有讲究。从数据结构和算法的角度拆分下来它大概落在这么几个大块里类别常见考点典型代表数组与哈希表两数之和、三数之和、字母异位词分组两数之和、三数之和双指针与滑动窗口有序数组中的去重、连续子数组、最小覆盖子串盛最多水的容器、无重复字符的最长子串链表反转、环检测、合并、相交反转链表、环形链表、相交链表二叉树与递归遍历、公共祖先、路径和二叉树的中序遍历、二叉树的最近公共祖先动态规划子序列、打家劫舍、跳跃游戏爬楼梯、打家劫舍、最长递增子序列图与搜索DFS、BFS、拓扑排序、岛屿问题岛屿数量、课程表堆与优先队列TopK、合并K个有序链表前 K 个高频元素看这张表你会发现真正的考点其实并不多来回就是这些模型。HOT100的厉害之处在于它把这些模型用100道题给串起来了。我刷完第一轮再回头看整个知识体系已经不是“我刷过某道题”的碎片状态而是“数组题大概就这么几种套路”“二叉树题无非就是遍历加递归处理”的框架感。这种框架感是做新题时最能救命的东西。2. 刷题前的准备路线规划比傻刷更重要2.1 三轮刷法第一遍求懂第二遍求快第三遍求稳我见过太多人一上来就给自己定“每天10题”的计划结果坚持不到一周就废了。原因很简单第一遍刷题时大部分题你都不会一天10道意味着每道题只有十几分钟思考时间最后全变成看题解抄代码抄完什么也没记住。我自己的经验是HOT100至少要安排三轮每一轮的目标都不一样。第一轮的目标是“搞懂每道题在考什么”。这个阶段不追求速度一道题花两三个小时都是正常的。实在想不出来就看题解但看完一定要自己重新写一遍不看答案写。这轮的关键是把每道题对应的解法、数据结构和复杂度都搞清楚并标注在一个刷题记录表里。第一遍如果能在六到八周内完成节奏就很健康。第二轮的重点是“熟练度和速度”。经过第一轮你已经知道每道题的大致思路了这轮要做的就是脱离题解尽量在20到30分钟内独立写出来。这个阶段你会发现自己第一遍写的垃圾代码到底有多臭也会开始对“模板”有感觉比如二叉树的遍历模板、双指针的移动逻辑、DP的状态转移写法这些都是可以固化成肌肉记忆的。第三轮则是“随机抽查和补漏”。这个阶段不要把100题按顺序做而是随机挑题模拟面试场景。我一般会用LeetCode的随机功能或者自己把题号抄在纸上抓阄抓到哪道写哪道。这轮的目的就是检验你到底是真的掌握了还是只是记住了题号对应的答案。2.2 按题型分类刷还是按编号刷这个问题我在不同阶段有不同的答案。第一轮刷的时候我强烈建议按题型分类刷而不是按题号顺序刷。原因很简单按题型刷能在短时间内集中理解一个套路形成类比记忆。比如你连续做10道双指针题做完之后看到“有序数组”四个字条件反射就会想到双指针这种反射是真的能形成肌肉记忆的。等到第二轮和第三轮就应该脱离分类做混合刷。这个时候按分类刷反而有害因为面试的时候没人告诉你“这题是DP的”你拿到题得自己判断该用什么方法。混合刷的目的就是训练这种判断能力。这里给大家一个我自己的分类顺序参考数组和哈希表最先刷因为它们是基础中的基础而且很多其他类型题都会用到哈希表然后是双指针和滑动窗口这两个是数组题的进阶玩法接下来链表链表的技巧性很强但考点固定刷起来成就感高二叉树和递归需要单独花时间因为它是面试里的重头戏而且和图论的DFS、BFS直接相关最后再啃动态规划因为DP需要前面所有知识的积累。2.3 别急着打开LeetCode先搭好舒适的工具链工欲善其事必先利其器。这句话在刷题场景里尤其成立。我最初是在LeetCode网页编辑器里直接写的说实话体验一般敲一会儿代码手就酸了而且本地没法保存自己的模板和笔记。后来我换成了本地配置的方式效率提升非常明显。我的配置很简单本地用VSCode写代码Python解释器用自带的3.10装一个Pylance插件做代码补全和语法检查。每道题我在本地建一个文件文件名格式是“题号_题目名.py”文件开头用注释写好题目描述、思路和复杂度然后再写代码。全部跑通后再把代码贴到LeetCode上提交。这样做的最大好处是你所有题解都被本地留存下来了复习的时候不用翻网页直接看本地文件就行。另外强烈建议安装pytest或者简单用if __name__ __main__的方式写测试用例。我见过很多人提交了好几次才发现逻辑有问题其实在本地把边界情况测一遍基本能避免大半的提交错误。这个习惯一旦养成写代码的准确率会明显上一个台阶。3. Python 刷题的几个核心套路提前练熟省一半力3.1 哈希表是默认武器Python的dict和set是刷题时最常用的数据结构没有之一。很多看起来需要两层循环的题用哈希表就能降到O(n)时间复杂度。最典型的就是“两数之和”这道题我在面试中被问过不止三次堪称HOT100里的题眼。def two_sum(nums, target): seen {} for i, num in enumerate(nums): diff target - num if diff in seen: return [seen[diff], i] seen[num] i return []这个代码的思路就是一句话遍历数组把已经见过的数字存进字典数字对应的下标作为值。对于当前数字只需要查一下“目标值减当前值”是否已经在字典里。这个查表操作是O(1)的所以整体是O(n)。我刚开始刷题时最容易犯的错误是先把整个数组存进字典再遍历这样做在数字重复时会出问题因为更新下标会覆盖。正确的做法是边遍历边存保证相同数字只保留最新下标这样即使有重复数字也不影响结果。这个“边遍历边查”的思路在后面的滑动窗口、连续子数组等一系列题里都能用到。3.2 双指针和滑动窗口数组类题目的主力数组类题目里双指针绝对是最重要的套路之一。它有两种基本形态一种是一个数组内从两端往中间走典型场景是有序数组另一种是快慢指针常用于链表和数组原地去重。“三数之和”那道题就是双指针的经典范例。排序后固定一个数剩余的用双指针从两端夹逼逻辑清晰还顺带解决去重的问题。我自己的经验是遇到“有序数组”“找一对数满足条件”“不重复子序列”这些关键词优先考虑双指针很大概率是正路。滑动窗口本质上是双指针的一种变体但它解决的问题类型很特殊——连续子数组/子串。比如“无重复字符的最长子串”和“最小覆盖子串”这两道题如果你用暴力法做复杂度是O(n²)用滑动窗口就是O(n)。代码模板其实就是维护窗口的左边界和右边界右边界不断扩张当窗口不满足条件时收缩左边界。def length_of_longest_substring(s): window set() left 0 res 0 for right, ch in enumerate(s): while ch in window: window.remove(s[left]) left 1 window.add(ch) res max(res, right - left 1) return res这段代码里window集合存的是当前窗口内的字符right每往右走一步就检查当前字符是否已经在窗口里如果重复就通过移动left把重复字符从窗口里挤出去。这个模板应对大部分“最长不重复子串”类问题都够用了。3.3 二叉树和递归先写框架再想细节二叉树题在HOT100里占比不低基本绕不开三个遍历前序、中序、后序以及层序遍历。刚开始碰二叉树时我总觉得递归看起来像魔法后来发现只要你把递归的“终止条件”和“对当前节点的操作”想清楚剩下的事情就是相信函数本身。拿层序遍历举例这是二叉树里最常考的“框架题”用BFS加队列来写是最舒服的from collections import deque def level_order(root): if not root: return [] res [] q deque([root]) while q: level [] for _ in range(len(q)): node q.popleft() level.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) res.append(level) return res这里有几个细节值得注意len(q)一定要在for循环前取因为循环过程中队列长度会变deque的popleft是O(1)但如果你用Python的列表pop(0)那是O(n)操作数据量一大会卡到怀疑人生。这个坑我在初刷时踩过后面专门整理在问题篇里了。二叉树递归题的另一个重点是“返回值定义”。很多题看起来复杂其实只要想清楚“这个函数返回的是什么边界条件是什么”代码自然就出来了。比如“二叉树的最近公共祖先”这道题递归函数返回的是“当前子树里是否找到了p或q”然后根据左右子树的情况来做判断。这类题写多了以后你会形成条件反射先写if not root再分析当前节点再递归左右子树。3.4 动态规划先暴力再优化动态规划是HOT100里最让新手崩溃的板块没有之一。我自己的心得是DP题别一上来就硬推状态转移方程而是先写一个暴力递归再看递归里有没有重叠子问题有的话就用一个数组或者字典把中间结果存起来这就是记忆化搜索。从记忆化搜索再转化成自底向上的DP表就能看清状态转移方程的样子了。拿“打家劫舍”举例题目是一排房子每间有一定的金额但不能偷相邻的两间。边界条件是只有一间房子时直接偷两间房子时偷金额更大的那间对第i间房子要么不偷它最优解就是前i-1间的最优解要么偷它那第i-1间就不能偷总金额是前i-2间的最优解加上当前房子的金额。转移方程写成dp[i] max(dp[i-1], dp[i-2] nums[i])再简单一点只用两个变量滚动更新空间复杂度能压到O(1)。这就是DP题的典型节奏从暴力到记忆化再到滚动数组优化每一步都有章可循。HOT100里的DP题基本都遵循这个节奏所以我一直建议大家把DP题放在后面刷等前面数组、递归基础打牢了再来否则很容易直接放弃。4. 实操记录一组 HOT100 题目的完整复盘4.1 三数之和排序 双指针的经典模板这道题堪称HOT100里的钉子户考察频率非常高解法也很有代表性。题目本身不难一个数组找出所有和为0且不重复的三元组。如果直接三层循环复杂度O(n³)在LeetCode上绝对超时所以必须优化。我的做法是先排序然后固定第一个数剩下两个数用双指针从两端夹逼。核心代码是这个样子def three_sum(nums): nums.sort() res [] n len(nums) for i in range(n - 2): if nums[i] 0: break if i 0 and nums[i] nums[i-1]: continue left, right i 1, n - 1 while left right: total nums[i] nums[left] nums[right] if total 0: left 1 elif total 0: right - 1 else: res.append([nums[i], nums[left], nums[right]]) while left right and nums[left] nums[left 1]: left 1 while left right and nums[right] nums[right - 1]: right - 1 left 1 right - 1 return res这里有两个细节是关键。第一个是外层循环的去重如果nums[i]和上一个数相等就跳过因为同一位置枚举出来的结果会重复。第二个是找到一组解后要对left和right都做去重而且要同时移动两个指针否则会直接死循环或者得到重复结果。我第一次写的时候就是忘了处理第二个去重导致输出里出现大量重复三元组。复杂度方面排序是O(n log n)外层循环O(n)内层双指针O(n)整体空间复杂度O(1)不考虑答案占用的空间。这道题做完以后我顺手把“最接近的三数之和”也刷了用的几乎是一个模板直接秒掉这就是套路复用的价值。4.2 环形链表与相交链表链表的双指针思维链表题在HOT100里数量不算多但几乎道道经典。我自己觉得链表题最考验指针思维因为链表不像数组能直接按下标访问所有操作都建立在“当前节点”和“指针移动”上。环形链表的经典解法是快慢指针快指针每次走两步慢指针每次走一步如果存在环两者一定会相遇。这其实是数学上的“追及问题”当慢指针进入环以后快指针每走一步两者之间的距离就缩短1所以必然会追上。def has_cycle(head): slow fast head while fast and fast.next: slow slow.next fast fast.next.next if slow fast: return True return False这里有个容易踩的点循环条件必须是fast and fast.next因为fast.next可能为None如果你只判断fast下一轮循环里fast.next.next会抛空指针异常。我刷的时候还专门研究过快指针每次走2步慢指针走1步为什么相遇一定能发生在环内而不是其他位置道理其实很简单慢指针最多走一个环的长度快指针因为速度差在这个周期内一定能追上它。相交链表那道题也用了类似的双指针思路两个指针分别从两个链表头部出发走到末尾后跳到另一个链表的头部继续走。如果两个链表相交它们必然在交点相遇如果不相交它们最终会同时走到None。这个解法的巧妙之处在于两个指针走过的总路程相等都是两个链表的长度之和。不需要提前计算链表长度代码还非常短属于“背下来也不亏”的模板题。4.3 岛屿数量DFS 与沉岛法岛屿数量是图论里的基础题也是HOT100里DFS/BFS的入门代表。题目给一个二维网格1代表陆地0代表水要求统计岛屿的数量。这里“岛屿”的定义是上下左右相连的陆地区域。我第一次拿到这题时第一反应是“每次遇到一个1就加一次计数然后把它周边所有相连的1都变成0”。这个思路被叫做“沉岛法”因为它的核心操作是“把一个岛整个淹掉”。def num_islands(grid): if not grid: return 0 def dfs(i, j): if i 0 or i len(grid) or j 0 or j len(grid[0]): return if grid[i][j] 0: 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 count这个解法的时间复杂度是O(m×n)因为每个格子最多被访问一次空间复杂度是递归栈的深度最坏情况下整个网格都是陆地递归深度可以达到m×nPython默认的递归深度只有1000这种情况下会直接栈溢出。解决办法很简单把DFS改成BFS用队列做显示栈或者在系统允许范围调高递归限制。我在本地测试时特意构造了一个很大的全1网格递归版本直接崩了换成BFS后稳稳跑完。所以这道题我建议你至少写两种解法DFS练递归思维BFS练队列使用两种都能写熟练了图论题才算真正入门。5. 刷题中的常见问题与排查技巧实录5.1 超时99% 是复杂度问题刷题刷到一定阶段你一定会遇到“我逻辑明明是对的为什么LeetCode显示超时”的情况。这里面99%都是复杂度问题而复杂度问题常见于两类场景一是用了O(n²)甚至O(n³)的暴力解法二是用Python语言特性时选错了数据结构。我遇到过最典型的一幕有人花了好长时间写出一个“能跑对”的版本一提交就TLE然后就开始怀疑人生。其实只要看一眼代码问题往往立刻就能发现——比如在循环里用list.index()或者list.pop(0)这两个操作都是O(n)级别的一旦循环次数上来了整体直接爆炸。针对这类问题我整理了下面这个速查表现象可能原因排查方向代码运行超时暴力解法嵌套了多层循环看有没有办法降到O(n log n)或O(n)优先想到哈希表、双指针代码运行超时且用了list.pop(0)列表头部删除是O(n)换成collections.deque的popleft()代码运行超时且用了list.index()搜索是O(n)用dict建立值到下标的映射代码运行超时且用了DFS递归递归深度过大改成BFS或显式栈内存超限存储了过多中间结果尝试滚动数组、原地修改、节省空间说实话超时这关是刷题路上的一道分水岭。能注意到这些细节的人往往已经在用“复杂度思维”来写代码了而那些只关注“功能实现”的人会一直卡在同样的坑里。学会在动手前估算复杂度是刷题的最大收获之一。5.2 边界条件老是在空数组和单元素上翻车写代码的时候一定要把边界情况当一等公民。我自己在刷HOT100初期提交失败的案例里大概有三分之一是逻辑错了三分之一是复杂度过高剩下三分之一全是边界条件没处理好。最常见的边界问题包括数组为空、数组只有一个元素、目标值不存在、链表的头节点为None、树为空、二维网格的行或列为0。这些小情况往往只需要一两条if判断就能解决但如果你没做LeetCode就会用几百个测试用例中的一个空数组来给你上一课。我个人形成的习惯是写完主逻辑后立刻问自己三个问题——“输入为空时会发生什么”“输入只有一个元素时会发生什么”“输入的最小值或最大值会触发什么”这三个问题排查完基本能覆盖大部分边界场景。另外一个很实用的做法是把边界测试用例直接写在本地文件里每次写完代码先用这些用例跑一遍再上LeetCode提交能把报错率拉低很多。5.3 递归爆栈Python 的递归深度不是默认够用的Python的递归深度默认是1000对一些递归深度可能达到上万层的题目来说这就会成为炸弹。最典型的场景是二叉树退化成链表时递归深度会达到节点数量级如果这个树有一万个节点直接报RecursionError。解决这个问题有几种思路。一是显式修改递归深度限制代码开头写import sys; sys.setrecursionlimit(1000000)但我个人的体会是这个方法治标不治本因为你不知道数据会有多大而且过深的递归本身就是风险。二是把递归改成迭代用显式栈或者队列来做。三是换一个解法思路比如某些DFS题可以改成BFS。举个小例子我之前刷“路径总和”这类二叉树题时递归天然很顺手本来不需要考虑深度问题。但当我开始刷“岛屿数量”的全1大网格时递归栈就直接爆了。所以合理的习惯是能用迭代就用迭代不能再用递归。这能帮你避免后期很多实际中遇到的栈溢出问题。5.4 眼高手低只看题解不默写这个坑是最隐蔽也是最危险的。我见过不少人刷题方式是“打开题解看一遍觉得懂了关上下一题”。结果到了面试现场手放在键盘上发现什么都写不出来感觉脑子很懂但手指不认识这些代码。我自己的做法是不管一道题看起来多简单看完题解后一定会把编辑器清空自己从头到尾写一遍而且不要偷看代码。第一次默写时卡壳是很正常的卡壳的地方恰恰是你真正不会的地方。这些卡壳的点我会专门标记在刷题记录里第二轮复习时重点看。另一个极端是“只会背题号答案”。有些人刷到第二轮第三轮看到“两数之和”直接把答案背出来了但题目稍微变一下比如改成“三数之和”就完全懵了。为了避免这种虚假熟练第三轮随机抽题时我会有意识地去思考“这道题考的是哪个模型”“这个模型还适用于哪些题”而不是单纯回忆代码。6. 刷题记录应该怎么记才有价值我的复盘模板6.1 记录什么刷题记录不是让你抄一遍题解而是让你把做题过程中产生的判断、错误和总结留存下来。我自己的刷题记录表包含七个字段题号、题目名称、分类、核心思路、复杂度、错误点、关联题目。“核心思路”这个字段是给自己看的必须用自己的话写而不是从题解里抄。比如“三数之和的核心思路是固定一个数然后对剩余部分双指针夹逼”这样一句话就够了。写多了反而不会看。“错误点”是整个记录里最有价值的部分。我第一次刷题时基本每道题都有一两个错误点比如漏了去重、边界忘记判断、用了O(n)的pop(0)。把这些错误点原样记录在表格里第二轮复习时一眼就能看到自己的薄弱环节效率非常高。“关联题目”是用来构建知识网络的。这轮刷题最大的收获之一是发现题目之间是有血缘关系的比如“两数之和”的哈希表思路在“字母异位词分组”里也能用到“三数之和”的双指针模板直接平移到了“最接近的三数之和”。把这些关联写下来可以帮助你在面对新题时想“这道题长得像我做过的哪道”从而更快定位解法。6.2 复盘频率与重点记录是第一步复盘才是把记录变成能力的过程。我自己的复盘节奏是每天晚上用半小时快速浏览当天刷过的题的记录重点看错误点每周日对本周所有题目做一次总结把错误点归类看看自己是不是在同一个坑里反复摔倒。我发现一个有意思的规律很多人的错误点集中在某几类比如边界条件、去重、忘记排序、复杂度估算错误。如果你能提前知道自己容易犯哪些错做题时就会主动提醒自己犯错率会降低很多。这也解释了为什么复盘那么重要——它不只是事后总结而是帮你建立一套做题前的自我检查机制。另外复盘时的“重点”不应该是“回忆正确答案”而是“回忆当时的思路和现在的思路有没有差距”。如果过了两周你看到一道题还能讲清楚它的核心思路和复杂度那这一题才算真正掌握了如果不能就把它加入待复习清单。6.3 一个小小的模板参考放一个我自己的记录模板你可以直接复制到自己常用的笔记软件里格式可以随意调整关键是字段别缺| 题号 | 题目 | 分类 | 核心思路 | 时间复杂度 | 错误点 | 关联题目 | |---|---|---|---|---|---|---| | 1 | 两数之和 | 哈希表 | 边遍历边查字典 | O(n) | 先存整个数组会覆盖下标 | 三数之和 | | 15 | 三数之和 | 双指针 | 排序固定一个数双指针夹逼 | O(n^2) | 去重逻辑容易漏 | 最接近的三数之和 |如果你不喜欢表格也可以用Markdown的普通列表关键是“核心思路”和“错误点”一定不能省。我自己在GitHub上建了一个私有仓库专门存这些记录每道题一个md文件刷题的路上随时能查。这套方法坚持下来你会发现刷题不再是“碰运气”而是一个有迹可循、可以量化的过程。最后再分享一个小技巧这个技巧帮我走过了最难的瓶颈期如果你刷到某道题卡了超过两个小时别死磕去休息一下。不是所有题都值得在第一轮就死磕到底——把不会的题标记好过几天再回头做你会发现第二次看它的感觉明显不一样。HOT100刷完不是终点真正的目标是刷完之后你在面试现场能稳定发挥。我的个人体会是刷题这事儿慢就是快记录比做题更重要复盘比刷新题更重要这三句话我到现在还常跟身边人念叨。