刷到第60天,正好是1月19号这天。LeetCode面试经典150题单,我从大概11月底开始啃,中途断断续续、反复回退,今天终于把这150题完整过完第一遍。说“过完”,不是说每道题都能一遍秒杀,而是所有题都至少动手写过、整理解过思路、记录了错误点。这份题单在准备技术面试的人里几乎人手一份,但它到底适合谁、该用什么顺序刷、卡住了怎么处理,真正说得清楚的并不多。这篇文章就把我60天的实际过程拆开来讲,希望能给正在刷题或准备开始的人一点参考。
1. 为什么是“面试经典150”而不是其他题单
1.1 这份题单和其他题库的区别
市面上的刷题资源无非几类:按知识点分类的专项练习、公司高频题汇总、竞赛真题,还有LeetCode官方出的面试经典题单。我一开始也纠结过到底刷哪个,后来选了“面试经典150”,核心原因是它的分类结构最适合面试场景。
这份题单把150道题按知识点切成块:数组/字符串、双指针、滑动窗口、矩阵、哈希表、区间、栈、链表、树、二叉树、图、回溯、分治、Kadane算法、二分查找、堆、位运算、数学、动态规划。注意它把树和二叉树分开列,动态规划放在最后面,说明官方是从面试出题频率和认知难度两个维度排的。相比之下,“热题100”更偏高频但题目之间没有清晰的训练主线,刷完容易“见一道会一道,换一道就懵”。“面试经典150”更像一套教材,每个知识点给足典型题,刷完能自然建立解题框架。
我个人的判断是:时间紧张、只剩两周突击面试的人,先去刷高频题更划算;但如果是还有一两个月准备时间,想把算法基础真正补到能应对大多数面试题的水平,150题是最合适的性价比选择。
1.2 60天的进度安排和阶段划分
一天刷2到3道题,加上复盘,大概需要两个小时。这是比较稳的节奏。我见过有人一天刷10道,但那样基本是看题解背代码,过几天全忘。我的60天大致分成三个阶段:
- 前15天:主攻数组、字符串、双指针、滑动窗口、矩阵、哈希表。这类题是算法题里的“基础代谢”,几乎每场面试都会碰到,而且很多复杂题的本质就是“数组+哈希”的组合。
- 中间25天:主攻链表、树、二叉树、图、回溯。树和图是面试官最爱的题型,代码量大、递归思维要求高,需要整块时间反复练习。
- 最后20天:主攻二分查找、堆、位运算、数学、动态规划,以及之前标记出来的难题回炉。
刻意把动态规划放最后,是因为它需要前面积累的数组遍历、递归、状态分析能力作铺垫。上来就刷DP容易劝退。
2. 刷题顺序和知识点的重要性排序
2.1 高频题型金字塔
如果把这150题按面试出现概率排序,我个人会排成这样:
| 优先级 | 知识点 | 典型特征 | 面试中出现概率 |
|---|---|---|---|
| 第一梯队 | 数组、哈希表、双指针、滑动窗口 | 代码量适中,边界条件多 | 几乎每场必考 |
| 第二梯队 | 二叉树、链表、图、回溯 | 递归和遍历思想为主 | 约70%的面试会涉及 |
| 第三梯队 | 动态规划、二分查找、堆 | 有固定套路但需要大量练习 | 约40%的面试会涉及 |
| 第四梯队 | 位运算、数学、前缀和、区间 | 题型相对固定 | 偶见,但考到就是拉开差距的题 |
这个排序和我刷题前的预期不太一样。我原来以为动态规划是最重要的,实际刷下来发现,二叉树和回溯才是区分选手的关键。原因很简单:动态规划题目特征明显,看到“最多”“最少”“方案数”基本能往DP上靠;而二叉树题可以伪装成各种形态,比如“验证二叉搜索树”“最近公共祖先”“二叉树的序列化”,识别不出递归模型的人会毫无头绪。
2.2 核心章节的刷题重点
数组和双指针是地基。这个部分我刷得最细,因为后面几乎所有题目都隐含了数组思维。比如“接雨水”这类题,表面是双指针,实际还可以用单调栈、动态规划做,一题练三个思路。滑动窗口的题目则要重点掌握“窗口何时扩张、何时收缩”的判断逻辑,这里最容易漏边界。
二叉树是重头戏。递归三步法几乎能解掉大部分题目:先确定递归函数的参数和返回值,再确定终止条件,最后写单层递归逻辑。我当时把每道树题都强制按这个模子写,写多了以后,看到二叉树题目第一反应不再是害怕,而是先条件反射地把三个步骤列出来。
动态规划的练习重点不在“状态转移方程怎么来”,而在“怎么证明它是错的”。一个DP写法提交之后,如果没过,不是状态定义出问题就是转移漏了情况。我把每道DP题的失败提交都留档,后发现最常见的错不是复杂的优化问题,而是数组下标越界、初始状态没赋好这种低级错误。
3. 60天里最值得反复刷的几类题目模式
3.1 双指针与滑动窗口的模板化写法
双指针类题目,我在60天后总结出一个通用思考框架:
- 先问自己:这个数组/字符串是不是有序的?如果有序,左右指针大概率能用。
- 再问:题目要求的是连续子串/子数组吗?如果是,考虑滑动窗口。
- 最后问:能不能用空间换时间?用哈希表记录已经出现的数据是关键。
以滑动窗口为例,我的固定模板是:
left = 0 for right in range(len(nums)): # 扩张窗口,更新状态 update(right) # 当窗口不满足条件时,收缩左边界 while invalid_window(): remove(left) left += 1 # 记录结果 record()这套模板解决“无重复字符的最长子串”“最小覆盖子串”这类题时特别好使。关键窍门是:每次只处理一个右端点的扩张,左端点只在条件不满足时才动,这样不会乱。
双指针的核心细节更多。比如判断回文串时,指针移动要跳过非字母数字字符;三数之和要去重,指针移动时不仅要保证left < right,还要跳过与上一次相同的值。这些细节我在前两次提交里全踩过,到后面形成肌肉记忆了。
3.2 回溯和二叉树是递归思维的试金石
回溯算法的题目数量不算多,但变化很丰富,从排列、组合、子集到棋盘问题、单词搜索,都是同一套逻辑。我把它总结成模板:
def backtrack(路径, 选择列表): if 满足结束条件: 记录结果 return for 选择 in 选择列表: 做选择 backtrack(路径, 新的选择列表) 撤销选择这个模板帮我拿下了所有回溯类题目。真正容易出错的是“去重”和“剪枝”。排列问题要用used数组记录哪些元素用过了,组合问题去重要先排序再判断相邻元素是否相同,棋盘问题要在递归前判断是否越界。我把常见的去重场景整理成了一张速查表,贴在笔记里,回看时非常方便。
二叉树题目我没有用统一模板,而是按照遍历顺序分类练习。前序遍历适合“从上到下构建”,中序遍历在二叉搜索树里有天然的有序性,后序遍历适合“从下往上返回信息”。一旦能够根据题目要求快速选择遍历顺序,树题就成功了一半。
3.3 动态规划的“五步法”练习
动态规划我刷了两轮。第一轮只求能看懂题解,第二轮要求自己独立推导。我用的还是业界常见的“五步法”:
- 定义dp数组的含义(下标、状态)
- 推导状态转移方程
- 初始化dp数组
- 确定遍历顺序
- 打印dp数组检查是否与预期一致
其中最容易翻车的是第一步。很多题看似能用“一维dp[i]”解决,实际状态有两个维度。比如“编辑距离”“最长公共子序列”必须用二维dp;“单词拆分”虽然是一维,但需要配合哈希表查词。所以每拿到一道DP题,我会先把“状态”用中文完整写一遍,再写代码。这个习惯帮我减少了很多无效提交。
4. 实际的刷题流程、代码习惯与提效工具
4.1 一道题的标准处理流程
我给自己定的流程是:
- 读题后,先不急着看题解,自己尝试设计暴力方案,哪怕复杂度很高也要写出来。
- 在暴力解基础上思考优化点,比如能否用哈希表替代两层循环、能否用双指针减少一层循环。
- 独立思考30分钟,有思路就直接动手写;没思路就标记为“卡题”,当天看题解,但第二天必须不看题解重写一遍。
- 提交通过后,去题解区看一两个高票思路,找出比自己解法更优的方案,把差异记录到笔记里。
- 每周固定抽出一天,专门重刷本周记录中标记为“第二次才能写对”的题。
这个过程很慢,但很有效。刚开始一天只能完完整整走完一遍流程的两三题,到后面熟练了,一天能处理四题,因为很多题目套路相同。
4.2 代码习惯和复杂度意识
刷题过程中,代码风格同样重要。我在60天里逐渐固定了一套习惯:
- 变量名尽量用有意义的词,而不是i、j、k满天飞。写left、right比写l、r更容易在思考时降低认知负担。
- 每个函数开头先处理边界情况:空数组、空字符串、只有一个元素、nums长度为1。
- 提交前强制自己算一遍时间复杂度和空间复杂度,写在注释里。面试时这一步几乎是必问题。
有一类错误我反复栽跟头:Python里的浅拷贝问题。列表直接赋值给结果数组,后续修改原列表时会连带修改结果。比如子集、排列类题目,记录结果时必须写result.append(path[:]),不能直接append(path)。这种坑不看题解很难发现,属于典型的环境特性坑,我后面统一用深拷贝快照的方式解决了。
4.3 我用到的提效工具
我不太推荐过度依赖插件和可视化工具,但下面几个对我的帮助确实很大:
- 本地笔记软件按知识点建页,每道题记录:题目链接、我的思路、复杂度、第一次错误点、最优解法的思路。
- 刷题平台的提交记录自带统计功能,我每周看一次“通过率最低的知识点”,就知道接下来一周该补哪里。
- 用Markdown维护一份“模板速查表”,收录双指针、滑动窗口、回溯、二分、树的遍历等常用模板,复习时只看这份表。
5. 卡题、遗忘、心态崩塌:60天里踩过的坑和应对方法
5.1 卡题的正确打开方式
卡题太正常了。150题里我独立做出来大概只有70来道,其余全是看题解后理解的。卡题并不可怕,可怕的是“硬扛时间”。我以前刷题有个坏毛病,一道题没思路就死磕一两个小时,最后又累又挫败。后来调整成“30分钟无思路就看题解”,效率高了很多。
看题解也有讲究。菜鸟是抄代码,老手是看思路。我通常先看题解的“思路描述”部分,不看代码,然后关掉题解自己动手写。如果仍然写不出来,再回来看代码,看完后把题目标记为“需要隔天重写”。这么做能避免“眼睛会了手不会”的虚假满足感。
5.2 对抗遗忘的策略
60天跨度足够长,遗忘是必然的。我在第20天回头看前几天刷过的数组题,发现很多解法已经想不起来了,当时很崩溃。后来我用了两个办法:
第一,滚动复习。每天刷新题之前,花10分钟快速浏览前一天做过的题和笔记,不重写代码,只看思路。刷到第50天时,我已经能用10分钟过完前10天的所有题目。
第二,模式归类。与其记“哪道题怎么做”,不如记“哪类题用什么框架”。比如看到“两个有序数组求中位数”,核心是二分排除法;看到“最长递增子序列”,先想贪心+二分能不能做,再想DP。把题目抽象成模式,遗忘速度会慢很多。
5.3 时间不够时的取舍
如果只剩两三周就要面试,我会做三个取舍:
- 果断放弃“困难”级别里偏门题,比如某些需要高级数据结构的题目,宁可留着时间巩固二叉树和DP的基础题。
- “中等”难度题优先刷,因为面试考察最多的就是中等题。
- 每题只追求“能讲清思路”而非“最优解”。面试里最常见的情况是,面试官并不要求你一次写出最优解,而是想看你能不能从暴力优化到更优方案。这时候过程比结果重要。
5.4 常见问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 思路正确但提交超时 | Python循环太慢,或复杂度比预期高 | 检查是否有重复遍历;考虑用哈希表/双指针降维;少数情况用sys.stdin优化IO |
| 输出结果与实际不符 | 边界条件漏处理 | 先补空输入、单元素输入;打印dp或窗口状态 |
| 递归栈溢出 | 终止条件缺失或递归深度过大 | 检查终止条件;大型数据优先考虑迭代写法 |
| 数组越界 | 指针移动没加边界判断 | 在访问索引前先判断上下界,尤其在while循环里 |
| 答案正确但内存超限 | 创建了多余的大数组 | 用滚动数组、原地修改、位压缩 |
| “看题解能懂,自己写不出来” | 只是视觉记忆 | 隔天重写;把题解思路用自己的话写在笔记里 |
5.5 独家避坑清单
最后分享三个我在第40天之后才悟到的经验:
- 题目会做和能表达是两回事。刷到后半程,我每周抽两三天,用口述方式把解题思路讲给自己听,模拟面试表达。很多刷题多的人挂在表达上,代码写对却说不清为什么,这在面试里非常吃亏。
- 难度高的题对代码量的要求未必高,很多时候二十行以内就能解决。如果发现自己写了六七十行还没完成,大概率是思路偏了,停下来重想。
- 别在刷题数量上自我感动。day60这个节点,比进度更重要的是复盘质量。我做过最亏的一件事是连续三天猛刷新题,结果把前面积累的模板全忘了。定期的回溯比推进度更重要。
6. 这是60天的答案,不是终点
费了60天刷完这150道题,说句实话,到了最后几天,我的刷题速度已经上来了,但心态反而更谨慎。因为刷得越多,越知道自己不知道的还有很多。比如有些题我虽然能过,但要我讲清楚证明过程还是有点虚;有些DP的状态压缩写法,我还在练习。这份150题更像一份“地图”,刷完第一遍,我总算能看清地图上的主干道了,接下来要做的就是把主干道跑得更熟。
如果你也正在刷这份题单,或者正要开始,我建议你把“刷完”当成一个起点而不是终点:一边刷一边整理自己的模板库,定期回头重做错题,多给自己模拟面试表达的机会。一个月后你再回头看60天前的自己,会明显感觉到算法思维上了一个台阶。这个感觉,远比那个“day60”的打卡数字更有价值。