第109天,3月10日,LeetCode面试经典150这套题单,我刷到了第109个打卡日。说"刷到"其实不准确,因为我早就不是第一次做这些题了,但这一轮完全是按面试实战标准来的:每道题不只追求AC,还要能在不看代码的情况下把思路、复杂度、边界条件完整讲出来。如果你正在准备技术面试,或者也在坚持刷题库打卡,今天这篇记录应该对你有参考价值——我会把当天的题目复盘、这100多天踩过的坑、以及从刷题到上考场之间的能力评估方法都摊开来说。
1. 从day1到day109:这套题库的批量刷法
1.1 为什么选"面试经典150"而不是随机题海
市面上刷题资源太多了,动辄几百上千道,真正能老老实实从头做到尾的人少得可怜。我选面试经典150这套清单,第一原因是它题量克制,150道刚好处在一个"能刷完、又能刷透"的区间;第二是覆盖相对均匀,数组、链表、二叉树、图、动态规划、贪心、回溯这些重点题型都有,不会出现某个高频考点完全空缺的情况。
很多人觉得150道太少,怀疑够不够用。我自己的判断是,面试考的不是谁见过的新题多,而是谁能在有限时间内把题归到熟悉的模型上。经典150里的题目基本都是高频模型的代表题,比如链表快慢指针、区间合并、二叉树递归、DP状态机,把这些吃透,再遇到变体题时,至少能判断"这题大概想考什么",而不是直接懵掉。
选这套题库还有一个现实理由:它不偏门。很多冷门但困难的竞赛题并不能提升面试表现,反而消耗大量时间。经典150的难度曲线更贴近实际技术面试的分布,简单题和中难题的比例大约是三比七,练起来不会开头就被打爆,也不会舒服太久。
1.2 我的三轮刷题节奏
我从第一天起就没打算一遍刷完。一轮按知识点分类刷,每天5到8题,目标是AC并且记录两样东西:这题属于哪个类型、我第一次思考切入点是什么。分类刷的好处是能在短期内反复接触同一类解法,比如连续一周做二叉树题,递归套路很容易刻进脑子。
二轮改成混合乱序,模拟真实面试抽题场景。每天随机抽3到4道之前做过的题,每道限时45分钟,只记录卡壳的位置,不追求当天全部写完。这一步很关键,因为按分类刷的时候,看到树题自然就想到递归,可一旦题目混在一起,第一反应往往先判断"这是数组还是字符串还是树",类型判断失败是很多真实面试翻车的起点。
现在是第三轮,也就是第109天所在的阶段:重点题压缩刷。每天从清单里抽10到15道高频题,不写完整代码,只做三件事——说思路、说复杂度、指出最容易写错的那个边界条件。三轮下来,150道题等于过了三遍,但每遍的侧重点完全不同。这个节奏我实测下来是比较有效的,尤其适合在职准备的人,每天不用挤大块时间,20到40分钟就能完成一轮有效复习。
2. day109当天:经典150里最值得反复做的三类题
2.1 二叉树最近公共祖先:从朴素解法到一步到位
当天做的第一道重点题是二叉树的最近公共祖先。很多人第一次见这题会想到各种奇怪做法,比如记录根到每个节点的路径再找最后一个相同节点,或者用哈希存某个节点的祖先集合。这些方法空间复杂度高,而且写起来绕。最优解的思路其实一句话就能说清:从叶子往根回溯,某一节点左右子树分别找到了p和q,那当前节点就是最近公共祖先。
这个思路用后序遍历天然就能实现,因为后序遍历本身就是"先处理左右子树,再处理当前节点"。我当天复盘的写法如下:
def lowestCommonAncestor(root, p, q): if not root or root is p or root is q: return root left = lowestCommonAncestor(root.left, p, q) right = lowestCommonAncestor(root.right, p, q) if left and right: return root return left if left else right对着这段代码要说清楚一个关键点:递归函数的返回值不是"找到公共祖先",而是"这一侧是否命中了某个目标节点"。当左右两边的返回值都不为空,说明p和q分别出现在当前节点的两个子树上,当前节点就是分岔点,即最近公共祖先。时间复杂度O(n),每个节点只访问一次,空间是栈深度。
面试官这题最喜欢的追问是:如果题目不保证p和q一定存在,你的代码会返回什么?上面的写法这时会返回错误结果,所以更稳的做法是先在递归里做一次计数校验,确认两个节点都找到了再判断分岔点。这个细节我当时第一次就没答好,现在它已经写进我的易错清单了。
2.2 最大路径和:递归返回值的设计逻辑
第二道是二叉树中的最大路径和。题面里路径的起点和终点可以是任意节点,这就导致很多人第一反应是把每条根到叶子的路径求一遍,但那样完全覆盖不了"路径中间拐弯"的情况。正确解法是树形DP的经典套路,核心在于设计递归函数的返回值和更新答案的方式。
先看代码:
class Solution: def maxPathSum(self, root): self.ans = float('-inf') def dfs(node): if not node: return 0 left = max(dfs(node.left), 0) right = max(dfs(node.right), 0) self.ans = max(self.ans, left + right + node.val) return node.val + max(left, right) dfs(root) return self.ans这里的递归返回值代表:从当前节点出发,往下探到任意节点,能得到的最大路径和。加上的前两步里有个陷阱,就是取max(子树贡献, 0),因为贡献如果是负数,还不如不走这条分支,这直接解决了路径可以"不经过某个子树"的问题。而全局答案的更新逻辑是:把当前节点当作拐点,让左贡献、右贡献、节点值一起形成一条完整路径去竞争最大值。
我第109天重新做这题时,最大的感触是"返回值"和"全局答案"是两个完全不同的概念。返回值必须是一条能继续往上传的路径,不能分叉;全局答案则允许分叉。很多人代码写不对,就是这两个概念混在一起。理解了这一点,同类型的"每个节点做拐点更新答案"的题都能顺带解决。
2.3 买卖股票问题:状态机DP的通用写法
股票类题目是面试经典150里的常客,从最简单的只能买卖一次,到可以买卖多次、含冷冻期、含手续费,难度逐级上升。第109天我复刷的是含冷冻期的版本,核心是用状态机DP把"每天可能处于的状态"拆清楚。
直接写状态转移比背题解靠谱。我用的三个状态是:持有股票、不持有但处于冷冻期、不持有且不在冷冻期。逐天转移:
def maxProfit(prices): if not prices: return 0 n = len(prices) dp = [[0] * 3 for _ in range(n)] # 0: 持有股票 1: 不持有且处于冷冻期 2: 不持有且不在冷冻期 dp[0][0] = -prices[0] for i in range(1, n): dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] - prices[i]) dp[i][1] = dp[i - 1][0] + prices[i] dp[i][2] = max(dp[i - 1][2], dp[i - 1][1]) return max(dp[n - 1][1], dp[n - 1][2])状态定义清楚以后,转移几乎就是照着描述翻译:持有股票,要么继续持有,要么从不持有且不在冷冻期买入;不持有且冷冻期,只能是从持有状态卖出的第二天;不持有且不在冷冻期,则可以从前一天的不持有两个状态继承。股票类题目最忌讳背模板,因为每加一个条件状态就变一次,真正要练的是"把一句话翻译成一个状态数组"的能力。
这题我每次刷都能看出新的东西,尤其是冷冻期状态存在的意义:它让卖出后的第一天不能买入。很多人会误把"冷冻期"理解成第三态,其实它只是"不持有"状态下的一个子状态。想通这一步,状态机DP基本就入门了。
3. 刷到第100天后的三个低效陷阱(我踩过的坑)
3.1 在一眼就会的题目上花太多时间
前60天我有个很严重的毛病:喜欢反复刷简单题。为什么?因为AC率高,看着提交通过挺爽,一天下来很有成就感。但到了第80天回头一算,发现大量时间耗在了"字符串去重""数组求和"这类容易题上,而真正拉分的中难题,比如拓扑排序、状态压缩、树形DP,练习次数远不够。这是典型的舒适区偏差。
解决方式很粗暴:把简单题分成两类。第一类是第一次就能AC且思路一句能说清的,直接标记为"长期跳过";第二类是题目简单但边界极多的,比如链表操作、窗口移动、整数溢出,这些保留下来做快速热身。到第三轮,我每天的简单题热身时间不超过10分钟,剩下的精力全部投到中高难题上。
3.2 只AC不看最优解,面试追问就废
第二颗雷是只看不学。早期我做题的标准就是"提交过了就完事",哪怕自己写了个时间复杂度O(n²)的解法也不管。但真实面试里,面试官不会因为你把题做出来了就满意,他一定会问一句:"能不能优化一下?"如果脑子里只有暴力解,当场就会卡壳。
现在我的规定变了:每道题AC之后,至少要花三分钟看一遍最优解,重点不是看别人的代码,而是理解他从什么角度切入的。比如"双指针为什么能把O(n²)降到O(n)""前缀和怎么把区间和查询变成O(1)"。这些优化思路在面试里被追问的概率极高。第109天我复刷时,心态已经从"我会做"变成"我能说出为什么要这样写"。
3.3 不写解题笔记,复刷等于从头再来
第三个坑最隐蔽,它会在几周后悄悄伤害你。我第45天左右刷过的一道树题,当时觉得简单至极,可第70天随机抽到它时,我盯着题目完全想不起来当时怎么解的。那不是遗忘,而是我根本没做过有效的沉淀。
从第70天起,我强制给自己加了一条规则:每道题在笔记里写一行"题型+核心思路+易错点"。不写长篇大论,一行就够。比如"二叉树最近公共祖先——后序自底向上,左右都有则是分岔点,注意p/q可能不在树上"。第109天回头翻这些笔记,基本扫一眼就能唤醒全部记忆,复刷效率比对着题目重新推导快好几倍。刷题而不总结,等于只做题不学习。
4. 用这109天的积累,怎么判断自己可以约面试了
4.1 150题清单的完成度怎么自测
刷完不等于可以面试,这个道理我到第90天才彻底想明白。完成度不能只看"这题我做过没有",而要看"抽到这道题,我能不能在规定时间内独立完成并讲清楚"。我把自测分成了三个等级:
| 等级 | 表现 | 是否建议进入面试冲刺 |
|---|---|---|
| L1 | 看到题能判断出题型归属 | 还差得远 |
| L2 | 能口述思路并给出大致复杂度 | 可以开始模拟 |
| L3 | 能白板写完整代码,边界无误,还能解释为什么这样写 | 基本达标 |
自测方法是随机抽题,而不是挑会的做。我每周日会从150题里随机抽10道,不允许选择,抽到哪道就现场回答属于哪个等级。如果连续两周L3的比例超过七成,说明题单掌握得相当扎实了。第109天我做了一次自测,L3大概在六成左右,结论是还得再压一周重点题。
4.2 模拟面试和自我摸底方法
约正式面试之前,至少要完成几轮模拟面试,越接近真实环境越好。我的做法是找一位水平差不多的朋友相互出题:一个当面试官,从题库里当场抽题,另外一个对着白板边写边说思路,全程录音。45分钟一道题,结束后复盘语音,重点听自己是不是在"闷头写代码"。
千万不要闷头写代码,这是模拟中暴露出的最大问题。真实面试里,你必须在写代码的同时持续输出思路:"我准备用双指针,因为数组已经有序,左指针向右可以让和变大,右指针向左可以让和变小……"面试官要的不是沉默的代码机器,而是能沟通的工程师。第109天我做完模拟后明显发现,我的思路表达比代码本身要慢半拍,说明平时还是练少了。
如果找不到一起对练的真人,可以给自己开"白板模式":不运行、不编译,直接在一张纸上写完整代码,写完之后再手动走一遍测试用例。这种方式恐怖但极其有效,它逼着你在落笔之前就把逻辑理顺。
5. 打卡到109天的工具与复盘清单(可直接抄作业)
5.1 我每天刷题后的20分钟复盘模板
第109天复盘当天题目时,我用的还是这个固定模板,已经用了将近四十天。把字数控制在20分钟内完成不过分,也不会拖延睡觉时间。第一步先记录今日题量、题型分类和每题的用时,第二步专门写"卡壳点",这个比"我做了哪些题"重要得多,第三步抄一遍最优解的关键优化思想,最后再写一句话给第二天的自己,打算巩固哪个薄弱点。
模板大致长这样:
日期:3.10(day109) 今日题量:4道 题型:二叉树×2,DP×1,链表×1 卡壳点: - 二叉树最大路径和:忘了负数子树可以截断 - 含冷冻期股票:状态间的转移写混了 最优解亮点: - 路径和采用每个节点当拐点更新全局答案 明天计划: - 复刷链表快慢指针三道,重点听自己的口述这套模板花不了多少时间,但长期积累下来,它就是一份完整的个人错题集。我第109天翻到第75天的笔记,还能清楚看到当时在滑动窗口上的问题,再去复刷那道题时,效率比盲刷高非常多。
5.2 给下一阶段的三条调整建议
如果按我现在的进度继续走,下一阶段有三个必须调整的地方。第一,把"写题"为主改成"讲题"为主,每天挑两道题用口述方式过一遍思路,甚至可以录下来自己回听,这一步直接补上模拟面试暴露的表达短板。第二,去看英文题目描述,面试时题目可能直接用英文给出,平时不习惯的话光读题就会慢半分钟。
第三,开始整理"一题多解"和"多题一解"。所谓多题一解,是指把能用同套思路解的题归到一组,比如股票家族题都归到状态机DP,链表环形相关题都归到快慢指针;一题多解则是同一道题体会暴力解、常见解、最优解之间的演进路径。到这个阶段,题目数量已经不是关键,能不能把模型和变体串成一张知识网才是拉开差距的地方。
另外推荐把刷题记录放在私有Git仓库里,不仅帮你保留打卡轨迹,还能方便随时用工具统计各题型的练习次数。这些细节看起来不起眼,但对我坚持到109天帮助很大。
第109天结束了,明天是110天。回顾这109天,我觉得最有成就感的时刻不是哪道题AC的那几秒,而是某天合上笔记,发现自己能直接说出"这是树形DP,自底向上,O(n),边界在负数子树"——这些压缩过的判断回路才是我真正想从题库里拿走的东西。算法面试准备到最后,拼的从来不是见过的新题多不多,而是常见模型有没有变成条件反射。如果你也在这条路上,稳住节奏,每天推进一点点,到某个节点你真的会突然发现自己能打一场像样的技术面了。