3月13日,day111,我还在LeetCode面试经典150的坑里爬着。说实话,坚持到第111天再回头看,最难的从来不是某一题有多绕,而是你能不能在一个周四的晚上,打开编辑器,把一道已经看过无数遍的栈题再老老实实写一遍。今天刷的是两道很有代表性的题——224基本计算器、875爱吃香蕉的狒狒,一个考栈的状态维护,一个考二分的边界感。正好赶上周末刚打完周赛430,顺便把复盘也做了。这篇文章就把当天怎么安排、两道题怎么拆、哪些坑值得记下来,逐条讲清楚。
如果你是刚开始刷题的人,面试经典150是比较合适的切入点,它覆盖面够广、难度梯度拉得开;如果你已经刷到一半,正在瓶颈期,我的第111天记录可能更值得看——因为长期刷题真正的瓶颈不是智商,而是节奏和复盘。
1. 111天刷题计划:为什么我把目标锁死在面试经典150
1.1 这150题到底覆盖了什么
很多人一开始刷题会有个误区,觉得热搜榜上的热门100题就是全部了。我前30天刷的就是热门100题的散装版本,今天一道链表,明天一道动态规划,完全是随机跳转,导致我每次看到新题都觉得“这题高考没考过”。后来转到面试经典150,才把系统性问题解决了。
面试经典150这150道题不是简单凑数,它的题型分布是有讲究的:数组和字符串、哈希表、双指针、栈、滑动窗口、矩阵、区间、图、树、二分查找、回溯、动态规划、贪心、位运算,基本上把面试里能碰到的算法考点分成了清晰的主干。比热门100题多出来的部分,主要在图论基础、区间合并、子序列模型、数位DP这些地方,这些恰恰是国内大厂笔试和面试环节很喜欢出的方向。
所以第111天我还在这套题里打转,不是因为刷得慢,而是因为每个专题我都要求自己过两遍:第一遍是看题解AC,第二遍是隔一周不看题解重写。150题按专题拆下来,基本上就是30多个专题,沿用固定模板也不现实,所以我坚持用“专题+错题二刷”的方式推进,3月13日属于栈和二分两个专题的交叉复习日。
1.2 第111天的当日报表:我实际怎么安排
长期刷题最忌讳的就是每天猛刷到凌晨,然后歇两三天,这样是无效努力。我第111天的安排是这样的,你可以直接抄作业:
- 20:00-20:40,重新手写224基本计算器,要求不看之前代码,并加注释说明每一步在做什么。
- 20:40-21:20,完成875爱吃香蕉的狒狒,重点调试二分边界,记录两种模板的差异。
- 21:20-21:50,复盘周末周赛430的T2和T3,把当时写错的边界条件抄进错题本。
- 21:50-22:00,翻看前7天的错题记录,抽查一道关于区间合并的二刷题。
这套节奏的核心逻辑只有一条:每天的重点是“稳定输出”,而不是“新的刺激”。刷到111天,我已经承认一个事实——很多题你看懂了不代表你会写,你当时会写了不代表这周还会写。所以当日报表里专门留了二刷时间,这一小时效率比新题高得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基本计算器(224题):栈里存的不是数字,是“现场”
2.1 先想清楚“括号会带来什么”
224题算是一个老熟人了,面试经典150必刷栈题。题目要求很简单:给定一个只包含 + - ( ) 和空格的字符串,算出表达式的值。没有乘除,没有负数开头的坑,看起来比课后习题还温和,但第一次写的人基本都会在括号这里卡住。
问题在于:括号的出现打断了表达式从左到右的求值过程。比如 1 + (2 - (3 + 4)),你在算到减号后面的左括号时,其实不知道这个括号整体对当前结果的影响是什么。你手算的时候会先算括号里面,再回头乘上括号前的符号,而这种“先算后面、再回过头来修正结果”的行为,用普通变量是没办法维护的。
这个时候栈就是唯一合理的工具:栈的作用是保存“进入括号之前”的计算现场。把现场压栈,进入括号后开一个新账本继续算,遇到右括号,把当前结果按括号前的符号处理一下,再和之前压栈的现场合并。这个思维模式,和开发里调用函数时的调用栈、断点保存寄存器现场是一模一样的,理解了这一点,224就不是背代码,而是顺逻辑。
2.2 代码拆解与关键细节
我的写法是比较通用的单栈写法,先把字符串里的空格去掉,然后一个字符一个字符地遍历:
python复制def calculate(s: str) -> int:
s = s.replace(' ', '')
stack = []
num = 0
sign = 1
result = 0
for ch in s:
if ch.isdigit():
num = num * 10 + int(ch)
elif ch == '+':
result += sign * num
sign = 1
num = 0
elif ch == '-':
result += sign * num
sign = -1
num = 0
elif ch == '(':
stack.append(result)
stack.append(sign)
result = 0
sign = 1
elif ch == ')':
result += sign * num
num = 0
result *= stack.pop()
result += stack.pop()
return result + sign * num
这套代码里有三个细节值得单独拎出来说:
第一,数字的拼接。因为例子里不会给你 <空格>12 这种中间态?会的,而且字符串里数字可能是多位,所以每遇到一个数字字符,都要 num = num * 10 + int(ch),这是老生常谈,但很容易在写括号分支时漏掉 num = 0 的重置。
第二,遇到括号时压栈的顺序:先压 result,再压 sign。这样出栈的时候刚好反过来,result *= sign 先搞定符号,再 result += stack.pop() 恢复之前的累计值。顺序写反的后果就是符号被加到结果上,整道题直接产出错误答案。
第三,为什么最后要 return result + sign * num。因为循环结束之后,最后一个数字可能还没被并入 result,必须手动补一次。如果遇到 ) 结尾,其实 num 已经被处理了,此时加一个 sign * num 也不会出错,所以统一写这行是安全的。
224的复杂度是 O(n),空间是 O(n),在面试的时候如果能顺口说出“这个栈深度取决于括号嵌套层数,最坏情况就是全部嵌套”,会显得你真的理解而不是背题。
2.3 面试里最常见的三种变形
只看224本身是不够的,面试官一般会在你写完这道题后立刻出变形。我第111天复盘的时候把常见变形整理成了表格:
| 变形方向 | 典型题目 | 核心变化 | 应对思路 |
|---|---|---|---|
| 加入乘除 | 227 基本计算器 II | 运算优先级变了,乘除要立即结算 | 用栈保存中间结果,乘除直接和栈顶运算 |
| 只保留加减和括号 | 224 原题 | 把加减翻译成正负号即可 | 用一个sign变量维护当前符号 |
| 表达式转逆波兰 | 150 逆波兰表达式求值 | 已经拆好token,无括号 | 遇到运算符就弹出两个操作数计算 |
我个人建议的练习顺序是:先224,再227,最后碰一下和逆波兰表达式相关的题。这三道做透了,你基本上就能跟面试官顺畅聊清楚“表达式求值”这个专题的来龙去脉。不要觉得这是冷门考点,字符串表达式处理在公司内部的配置解析、规则引擎、报表计算里随处可见。
3. 二分查找的边界实战:爱吃香蕉的狒狒(875题)
3.1 为什么这道题能成为“二分入门神题”
875题爱吃香蕉的狒狒,概念很可爱:狒狒要在 h 小时内吃完所有香蕉,每堆香蕉有若干根,狒狒每小时最多吃 k 根,如果一堆少于 k 根就一次吃完这堆然后休息,求最小的 k。
这类题和传统二分查找不一样的地方在于,它二分的是“答案”,不是“数组下标”。你在一个取值区间里猜速度,每猜一次就模拟一次吃完所有香蕉要多久,然后判断这个时间是否满足 <= h。这种“二分答案+单调性验证”的模式,是二分所有应用里最通用的一类,875则是理解这个模式的入门题。
为什么说它适合入门?因为单调性非常直观:吃得越快,耗时越短;耗时随速度单调递减。找到单调性,你就可以放心大胆地二分。你不需要关心香蕉具体怎么堆,你只要知道“当前速度能否满足时间限制”这个条件判断是可靠的就行。
这里顺便纠正一个网上流传的题号问题:很多人把爱吃香蕉的狒狒记成073,其实力扣上这道题的准确编号是875。073是矩阵置零,不是这道二分题。记错题号不算大事,但面试时对着面试官报错题号就尴尬了。
3.2 模板选择:我为什么坚持左闭右开
二分写法网上至少有三种流派,left < right、left <= right、left + 1 < right,各有各的道理。第111天我用875专门对比了一次,最终长期稳定下来的写法是左闭右开:
python复制def minEatingSpeed(piles, h):
left = 1
right = max(piles)
def can(k):
hours = 0
for pile in piles:
hours += (pile + k - 1) // k
return hours <= h
while left < right:
mid = (left + right) // 2
if can(mid):
right = mid
else:
left = mid + 1
return left
这种写法有几个好处。第一,left 和 right 的含义非常清楚:左闭表示当前可行区间里包含等于答案的情况,右开表示 right 是第一个“肯定不行”的位置,循环不变量好维护。第二,right = mid 不会把可行解排除,而 left = mid + 1 会排除所有“mid 不可行”的情况,这样不会死循环。
为什么会死循环?问题往往出在 mid = (left + right) // 2 配合 left = mid 上。当 left + 1 == right 时,mid 永远等于 left,如果条件成立,left 还是 left,无限循环。所以如果你的模板用了 left = mid,请老老实实把 mid 改成 mid = (left + right + 1) // 2,向上取整。这是二分最容易踩的暗坑,等下我在第5章再展开讲。
875的时间消耗计算也值得说清楚:can(k) 里对每堆香蕉计算耗时用的是 (pile + k - 1) // k,这是向上取整的整数写法。如果写成 pile // k,你会多算时间,一旦遇到 pile % k != 0 就直接错。这道题 h 可以很大,但 right 取 max(piles) 已经足够,因为速度再快也没意义——每小时最多只能吃完一整堆。
3.3 吃香蕉的完整实现与耗时分析
继续顺着875聊实现细节。can(k) 函数内部模拟整个吃香蕉的过程,但不需要真的逐小时模拟,直接从每堆香蕉的根数算出这堆需要几小时。比如一堆9根,速度是4根/小时,那一小时吃4根、一小时吃4根、剩1根还要一小时,实际耗时是3小时,恰好就是 (9 + 4 - 1) // 4 = 3。
如果堆数很大,模拟整个过程的复杂度是 O(n),二分次数是 O(log max(piles)),总复杂度是 O(n log max(piles))。这在面试里属于可接受的复杂度,但如果你能在写完后主动提一句“如果对每堆耗时可以用前缀和优化,那还能更快”,面试官会觉得你不只停留在背诵层面。
我在第111天的复盘里给自己留了个提醒:875是“最小值可行性”二分,和周赛430里那道“最大值不可行性”的题形成对比。很多人会混淆这两个方向,建议刷的时候把所有二分的范围搜索题放在同一个错题本里,专门比较 left = mid + 1 和 right = mid 的搭配。
4. day111复盘:3月13日的刷题节奏与复盘方法
4.1 一题多解的价值:不只是AC
第111天选择的这两道题,一栈一二分,表面上没有关联,但复盘的时候我发现它们有一个共同点:都有多种解法,而且解法之间的差异能帮助你理解数据结构的本质。
224除了栈解法,还有一个双栈解法和一个递归下降解法。双栈写法是表达式求值问题的通用解法,一个栈存数字,一个栈存运算符,遇到右括号就把栈里算到左括号为止;递归下降则更接近编译原理,把表达式拆成 expr + term + factor。刷到后期,我反而推荐大家把224的多解法都过一遍,不是显摆,而是面试时如果面试官追问“还有没有更好的解法”,你至少有话可接。
875也同理。二分模板虽然多,但核心只有一个:单调性、上下界、验证函数。把这道题用三种二分模板写一遍,比盲目刷三道新题更管用。
这里我有一个坚持了很久的习惯:每周日复盘本周所有题的“解法数量”,如果某道题我在15分钟内只能想到一种解法,我会把它放进“待二刷”列表。因为面试时很少有人只满足于最朴素的解法,做一题会三题才是刷题效率最大化的体现。
4.2 错题本怎么记才能复习
我的错题本不是简单抄一遍题解,而是按固定模板记录的:
- 错因类型:边界条件写错 / 状态定义不清 / 复杂度分析不了。
- 一句话思路:用自己的话把核心逻辑浓缩成一句话。
- 模板差异:比如这道二分题用的是左闭右开,和另一道用的左闭右闭有什么区别。
- 下次复习时间:一般是一周后,标记为“二刷待查”。
第111天翻错题本的时候,我发现最容易被记进错题本的不是那些难到天际的题,而是本来以为会写、结果一次写崩的基础题。所以如果你要建立错题本,别只记“我不会的”,更要记“我装会了的”。224这种题我大概隔两周就会重写一遍,每次写都能发现新的问题,比如某个分支忘了重置 num。
错题本的复习频率我建议按遗忘曲线走:当天、第三天、第七天、第十四天。我实际执行下来,第七天最有效,因为那时候你还没完全忘,但已经忘了一部分,重写正好能暴露漏洞。
4.3 周赛430带来的启发
3月13日当天正好是周四,周末的周赛430刚打完,热度还没散。我看了一下热搜里的周赛430相关讨论,很多人在T2和T3都栽了。我自己打的时候也犯了一个很典型的错误:T2按常规思路扫描数组,结果在边界比较上少判断了一个等于号,导致一个用例直接WA。
周赛带给我的最大价值不是排名,而是让我找到自己平时刷题和真实比赛之间的差距。经典150里的题大多有明确的输入范围,但周赛题会在边界上疯狂做文章。比如T3,几乎就是把某种数据结构加了一个环状条件,看起来像是在考代码量,其实核心还是你平时处理边界问题的熟练度。
所以我给想要提升的人一个建议:刷经典150的同时,周末打一场周赛,不管名次,只负责把每道错题写进错题本。刷题计划能不能持续,其实靠的是这种“受打击—复盘—再战”的循环。
5. 这111天踩过的坑,按类型给你列好
5.1 字符串题的三板斧
刷了这么多天,跟字符串相关的题踩坑最多,224就是典型。我的三板斧:
- 先统一清理空格,别在循环里到处判断
ch == ' '。 - 确定数字的起止标志,多位数字场景必须提前想到。
- 凡是涉及括号的,先想清楚“进入新上下文时,哪些状态要保存”,而不是急着写代码。
用224来说,如果你在写循环时还没想清楚 stack 里到底要保存什么,那你写出来的大概率是打补丁式的代码,一会儿这里加个判断,一会儿那里加个 if。我早期也这么写,后来发现一道表达式题写了80行还过不了全部用例,最后重写成标准单栈写法,30行就过了。
5.2 二分类题的边界急救
87%的人写二分要么死循环,要么答案差1,我自己前30天也是。这里放一个急救排查步骤:
| 症状 | 可能原因 | 急救方案 |
|---|---|---|
| 死循环 | mid = (l + r) // 2 + l = mid |
改成向上取整,或把 l = mid 改成 l = mid + 1 |
| 返回结果偏小 | 验证函数把可行解判成了不可行 | 检查 can(k) 的等号条件,比如题目要求 <= h 还是 < h |
| 区间初始范围太小 | left 只设0或者 right 只设 len 附近 |
按题意重新分析上下界,875的 right 设为 max(piles) |
我常用一个土办法:把二分过程中 left、right、mid 的每一步打印出来,拿一个小用例走一遍。875这种题不是竞赛压轴题,自己调试两三分钟就能发现规律,比死磕半小时效率高。
5.3 坚持111天的心理建设
最后聊点跟代码无关但比代码更重要的东西。能坚持到111天的人,一定不是因为每天都很热血,而是把刷题变成了像刷牙一样不用脑子的习惯。我在前期给自己规定“每天至少刷一道题,如果今天状态不好就只做一道简单题”,这个下限帮我扛过了很多加班晚归的日子。真到了第111天,我发现自己最大的收获不是AC记录,而是对“不会的题”没那么恐惧了。
你现在如果刚开始,别盯着“150”这个数字焦虑。按我的经验,前30天是最劝退的阶段,因为你会发现自己好像什么都不会。熬过30天后,会开始对题目分类,对各种模板有肌肉记忆,后面每多刷一天都在叠加复利。day111不是终点,它只是证明一件事:稳定输出,比间歇性爆发有用得多。
我个人的体会是,坚持刷题的日子里,最宝贵的不是抄了多少题解,而是那些写错后查了一晚上才搞明白的时刻。它们才是真正长在你身上的能力。如果你也在刷经典150,希望这篇记录对你有用,至少让你知道:第111天的人,跟你一样会踩坑,会忘题,但还是会继续打开编辑器。
