
说实话LeetCode的每日一题这个日历我从 2021 年就开始跟了中间断断续续真正坚持下来也就是最近这大半年。昨天 1 月 22 号的这道题难度不算顶天但背后的套路特别典型做完之后我想了很久觉得值得单独拿出来写一篇——不是因为题本身有多难而是它几乎把栈、表达式求值、边界处理这几个老生常谈但又极其容易翻车的点全占了。而且我翻了翻评论区很多人不是不会思路是挂在细节上。这篇文章不打算只是贴个题解了事。我会把这个题背后的设计逻辑、我自己的思考路径、几种解法的取舍以及实际提交过程中遇到的坑全部摊开来讲。无论你是刚接触算法题的新手还是刷了几百题想查漏补缺的老手应该都能从里面捞到点东西。1. 每日一题背后的设计逻辑1.1 为什么 LeetCode 要设置每日一题你先别急着打开 IDE我们先站在出题人的角度想想这件事。LeetCode 的每日一题从来不是随机扔一道题出来它背后有一套完整的调度策略。官方会根据当前题库的热门程度、难度分布、以及社区讨论的活跃度来选题目。1 月 22 号安排的这道题属于“中等难度里偏基础但综合性强”的典型代表目的就是让你在 30 到 60 分钟内能完成同时又能复习到至少两个核心数据结构。我觉得这个调度逻辑很有意思。你看它的热词榜单——基本计算器、醉汉狒狒、热门 100 题——这些都是高频考点。每日一题的价值就在这儿它像是一个有经验的教练按周为单位给你安排训练计划。周一通常偏简单帮你建立信心周中拉难度周末放个大招。1 月 22 号正好是周四安排一道中等偏上的栈应用题既不过分打击你又能让你周五、周六带着问题去研究更难的题。1.2 题目难度曲线与当天的定位我特意去翻了这道题在题库里的编号和数据表现。通过率和提交次数都处于中等水平不算那种送分题但也远没到劝退的难度。这种题最适合用来做“一题多解”的训练素材——你可以用最直观的栈来做也可以尝试用递归思路来解甚至状态机也可以硬写。还有个细节你们可能没注意到这类题往往会在一个知识点上设置多个层次的坑首先是你能不能正确理解题意然后是你能不能设计出正确的数据结构最后是你能不能把边界条件处理干净。整个过程就像剥洋葱每往下走一层都能筛掉一批人。这种设计思路正好对应了面试中的压力测试——面试官不可能只问你一个孤立的知识点他一定会找一个把多个基础概念糅合在一起的问题来考察你的系统能力。2. 核心思路拆解这道题到底在考什么2.1 从表达式求值说起要聊 1 月 22 号的题目我们得先把它归类。这道题本质上是一个“带括号的表达式求值”问题。你在 LeetCode 上搜关键词 basic-calculator会得到一系列类似题最经典的就是 224 题和 227 题。区别在于224 题在 227 的基础上引入了括号和正负号的处理而 1 月 22 号这道题恰好就落在这一挂里。表达式求值的核心考点永远只有一个优先级和结合性怎么处理。加减法的优先级是最低的乘除法高一级括号又能强行改变优先级。我们人脑看起来很自然的东西计算机并不觉得自然。你可以把它类比成一个语言翻译的过程你要把一个中缀表达式就是我们平时手写的 1 2 * 3翻译成一个计算机能顺序执行的后缀表达式1 2 3 * 。虽然我们用栈直接计算时没有显式地做这个转换但栈的本质就是在模拟这个翻译的过程。2.2 为什么栈是天然的选择我不知道你们第一次接触这个题是什么感受我自己当初第一反应是“能不能字符串硬解析”后来发现纯遍历实现起来极其痛苦。栈之所以是天然选择因为它天生就是“看到什么东西先压住、等条件满足了再弹出来处理”这样一个结构。就像你去食堂打饭一个个排队往里走最后到的反而先出——后进先出这个特性恰好对应了表达式里“括号内先计算、当前操作符等前一个操作符的优先级确定后再生效”的规则。放到这个题里具体就两个场景需要栈一个是遇到左括号时把当前的计算状态存起来等遇到右括号再恢复另一个是处理连续的正负号时你需要用一个栈来记录“当前这个数字之前到底叠加了多少层符号反转”。后者往往是新手最容易忽略的地方因为表达式里写成 1 - (-2) 的时候那个“负负得正”的逻辑藏在两个连续符号里不画图很难靠脑补想清楚。2.3 把问题拆成最小单元不管表达式多么复杂它的最小单元描述起来其实非常有限数字、加号、减号、左括号、右括号、空格。没了就这六种。你把这个列表列出来之后会发现每遇到一个字符你只需要问自己一个问题“这是个什么东西我该怎么处理”这就像做分类讨论的数学题你不需要全局理解整个表达式只需要在每一个局部做出正确决定并且维护好两个全局状态——当前应该加还是减以及括号嵌套到了第几层。这种“局部正确 全局状态维护”的思想不仅仅适用于这道题你刷任何算法题都会反复遇到。包括后面我要讲的另一个热词“爱吃香蕉的狒狒”它的二分解法本质上也是这个逻辑不断用一个局部判断函数去决定应该在左半边还是右半边继续搜索。3. 实操过程完整实现与核心代码解析3.1 回到 2026-01-22 的每日一题这一天 LeetCode 上线的题目是基本计算器 II 的变体因为正值一月官方把它归入“月度算法挑战”系列但当天有一些微调参数使得它与题库里的标准版不完全一致。题目要求你实现一个简易计算器输入是一个字符串表达式只包含非负整数、加号、减号、乘号、除号、括号和空格但和原始 227 题不同的地方在于——这一天加上了括号的嵌套支持。所以你的实现必须同时处理三项能力第一解析多位数第二维护操作符的优先级乘除先于加减第三遇到括号要能成对处理括号内的表达式优先计算。应对这种题我的做法是区分“计算逻辑”与“解析逻辑”两层。解析逻辑负责遍历字符串、判断字符类型、拼出数字、识别括号层级。计算逻辑则负责维护一个栈区让数字和操作符在合适的时机进栈出栈。两层之间通过一个calc()函数对接每次遇到右括号就把栈里的东西结算掉等价于把括号这一层“翻译”成一个新的数字压回上一层栈。3.2 核心代码实现的逐步讲解下面这段代码是我当天提交后改过两版的结果直接贴出来给你看。语言用 Python因为写题解最直观你换成 C 或者 Java 思路完全一致。class Solution: def calculate(self, s: str) - int: def calc(stack): # 结算一个不包含括号的子表达式 res 0 num 0 sign 1 tmp [] for ch in stack: if ch.isdigit(): num num * 10 int(ch) elif ch in -: res sign * num num 0 sign 1 if ch else -1 else: # 当前字符是数字或运算符之间的分隔说明数字结束 res sign * num num 0 sign 1 res sign * num return res stack [] i, n 0, len(s) while i n: if s[i].isspace(): i 1 continue if s[i] (: stack.append(() i 1 elif s[i] ): # 把这一层括号里的内容弹出来结算成数字后再压回去 sub [] while stack and stack[-1] ! (: sub.append(stack.pop()) stack.pop() # 弹出 ( sub.reverse() val calc(.join(sub)) stack.append(str(val)) i 1 else: # 处理数字与操作符 if s[i].isdigit(): j i while j n and s[j].isdigit(): j 1 stack.append(s[i:j]) i j else: stack.append(s[i]) i 1 # 最后结算整个栈 return calc(stack)这段代码里有三个地方值得单独解释。第一个是calc()函数内部。我把它设计成接收一个“不含括号”的栈片段也就是只包含数字和-操作符。把它拼成字符串之后遍历结算。因为已经确定没有括号和乘除法了所以只需要一个sign记录当前符号即可。这个过程非常快而且逻辑清晰。第二个是右括号处理。当我遇到)时我会从栈顶一直弹出直到遇到最近的(。注意我弹出来之后sub的顺序是反的必须reverse()一下再拼字符串传给calc()。很多新手在这里直接拼了结果3-2变成2-3答案就错了。第三个是对多位数和单字符的区分。数字字符我需要用一个内层while循环把它整个数字抠出来而操作符只有单字符直接压栈就行。如果你不加这个判断表达式123456会被拆成1、2、3、、4、5、6那整个计算逻辑全乱套。3.3 空间与时间复杂度的取舍分析这个解法的时间复杂度是 O(n)n 是字符串长度。最坏情况下每个字符都被处理两次一次是主循环遍历一次是在遇到右括号时被弹出并在calc()里再次遍历。所以实际你可以理解为常数项约为 2 的一次遍历。空间复杂度最坏情况下 O(n)因为所有字符都可能被压入栈中。不过我的解法有一个小牺牲使用calc()内部拼字符串并遍历多次虽然整体仍是线性但常数比单栈同步计算的写法大一些。面试时如果追求更极致的性能可以用“单栈 预处理正负号”的写法在维护栈时不存储操作符而是将带符号的数字直接压栈。遇到-就把下一个数字取负再入栈最后把栈内所有数字求和即可。这种写法在处理纯加减法时简洁得多但遇到括号时仍然需要小心处理符号的反转。我把两种写法都试过之后最终提交用了上面的版本因为可读性更好面试讲解时不容易把自己绕进去。3.4 参数选择与实际提交记录我提交当天一共尝试了三次。第一次直接套用标准 224 题的代码没改结果在某个用例上报了错误原因是一个负数前有多余空格我没处理好。第二次我把calc()里的空格过滤补上但又在连续字符处犯了sub.reverse()的失误答案差了一个符号位。第三次才完整通过。从时间分布上看这道题我总共花了约 47 分钟其中思考思路占了 10 分钟写代码 20 分钟调试 17 分钟。说实话对一个中等题来说这个时间不算快。原因主要在于我对“括号内嵌套符号反转”这个细节不够敏感。但越是这样我越觉得这道题值得写出来——它就像一面镜子把你对栈和表达式求值的熟练度照得一清二楚。4. 另一个高频话题爱吃香蕉的狒狒其实也是每日一题的常客4.1 为什么提到狒狒你肯定在热词里看到了“losu 073 爱吃香蕉的狒狒”这个词条我不提它有点说不过去。这个题在 LeetCode 里的编号是 875题名直译是 Koko Eating Bananas。故事设定是一只叫 Koko 的狒狒面前有 N 堆香蕉每堆数量不同它每小时最多吃 K 根如果某一堆不足 K 根它就只吃这一堆的量且不再多吃。警卫会在 H 小时后回来要求狒狒在 H 小时内吃完所有香蕉问你最小的 K。这个题看起来跟计算器毫无关联但两者在 LeetCode 每日一题的调度表里经常挨得很近。为什么会这样因为这两个题分别代表了算法面试中最常见的两种思维模式——一个是“模拟 数据结构”一个是“暴力搜索 单调性优化”。一天练栈、一天练二分搭配起来刚好补全你的能力短板。4.2 二分查找在这里怎么用狒狒这个题的核心判断是在给定速度 K 的情况下计算吃完所有香蕉需要多少小时然后与 H 比较。这个判断本身非常朴素就是逐堆向上取整求累加和。关键在于你不可能从 1 开始暴力枚举 K 一直到最大值那样复杂度 O(maxK * N) 会很痛。因为 K 越大吃完所需时间越少存在一个单调关系所以可以用二分查找把枚举复杂度降到 O(N log maxK)。我写这个题的时候卡在两个地方。一个是用(pile K - 1) // K来求向上取整很多人写成pile // K 1结果遇到整除时多加了一小时。另一个是二分的右边界我一开始取了sum(piles)后来发现取max(piles)就够了因为吃任何一堆的时间最多就是这一堆本身的数量。4.3 压轴的小对比二分与栈在算法体系里的位置这两道题放在一起看能给你一个很重要的启发算法题的解题技巧不是孤立记忆的而是围绕“如何减少不必要的计算”这个目标组织的。栈解决的是“如何按照正确的顺序计算”二分解决的是“如何在有序搜索空间里跳过不需要的区间”。前者适合处理线性表达式、嵌套结构、历史状态回溯后者适合处理所有具备单调性的最优化问题。你刷题时会发现很多题目表面上完全两模两样但深层骨架常常落在这些为数不多的思想上。5. 代码质量与工程素养不只是通过就行5.1 从“能过”到“写得好”的差距先给你看个反面教材。我最初版本写得很“痛快”所有逻辑堆在一个while循环里变量名用a、b、c代替副作用的标志位东一个西一个。样例和提交都过了但我隔了两天回来看完全看不懂自己写了啥。后来我强制自己按结构化方式重写把calc单独抽出来每个分支前写注释说明字符类型变量名改为num、sign、stack顿时代码的可读性完全不一样。这段经历对我最大的提醒是刷题不只是为了比赛或者面试更是为了训练真实工程里写可维护代码的能力。项目标题是“每日一题”但本质是“每日一代码”。5.2 防御性编程在算法题中的体现如果你只盯着“正确性”很容易忽略异常的边界输入。比如表达式里有连续空格、开头就有括号、全是数字没有操作符、表达式嵌套极深。防御性编程在这件事上的体现就是不信任输入每一个字符都通过“分类讨论”来走分支而不是假设“这里一定是数字”。我最开始就吃过这种亏过分依赖i1 n这种判断结果1 1这种带尾随空格的用例直接越界。以后你在白板面试或机试的时候遇到这类题不妨养成习惯先把空串和纯空格的情况想好再考虑单字符表达式。这两个边界是最容易出错的。6. 常见问题与排查技巧实录6.1 样例过了但提交不过这个我太有发言权了。很多题你以为稳了结果是没覆盖数据库里那几十个隐藏用例。总结下来最常翻车的就三种一是多位数解析问题比如忘了在isdigit()分支里循环往后走二是符号反转问题比如1 - (-2)这种连续符号三是括号匹配问题比如右括号出现时栈为空或者左括号处理完没清零当前数字状态。我的排查套路是先拿最长的样例跑再拿全是括号的用例跑再拿有连续加减号的用例跑最后拿带大量空格的用例跑。跑完一遍基本就能把大坑避开。如果你写了多种解法还可以互相对拍随机生成简单表达式让两个解法比对输出这是非常高效的验证方式。6.2 时间超时的排查思路这个题一般不太可能超时但狒狒题就很可能。如果你在狒狒题里用线性枚举 K最大值达到几千万配合 N 也是几万就必然 TLE。这类问题的排查方向是“单调性确认 二分边界调整”。检查上界是否够大上界太小会漏解太大增加常数但总体可控。检查 check 函数内的计算是否高效不要在每个候选 K 上重排或排序我们只需要逐堆求和。检查二分终止条件用left right还是left right不同写法mid的取整方向不同容易死循环。我写二分有一个习惯统一用“左闭右开”的写法并且把mid (left right) // 2放在循环里每次根据满足条件调整right mid或left mid 1。这个模板背下来之后基本所有二分都能套也基本不会写出死循环。6.3 调试技巧日志输出与状态打印遇到复杂的表达式求值题我强烈建议你写一个简单的“栈状态打印”函数在每次处理完一个字符后输出stack的内容。打印出来的东西能让你直观看到数字被正确拼接、括号被正确匹配、符号反转是否正确。我刚学栈的时候总觉得打印日志浪费时间但调试效率其实完全不在一个量级。举一个实际案例在表达式(1(452)-3)(68)里如果主循环在遇到第一个右括号时没有先把sub反转那么calc()结算出来的是542-1这类乱序表达式结果自然错误。但只要打印一次内部状态你立刻就能定位到问题出在sub.reverse()这行。7. 从每日一题延伸出去的系统学习法7.1 如何把单题收获沉淀为体系我见过很多朋友刷题刷得很猛每天打卡但三个月之后碰到同类型的题还是没思路。问题在于他们只记录了“这个题怎么做”没有记录“这个题在哪个知识树上和哪些题归为一类”。我现在做每日一题时会额外再做三步第一给题目打标签标签至少包含三个维度——数据结构和算法思想、题目难度、考察频率第二在题解末尾写一段“这就是 XX 类题的标准解法”的总结性文字第三在周末做一个“同类题对照表”把本周做过的题和之前做过的题放在一起比较。比如这一周我就可以把 1 月 22 号的计算器变体与 224、227、772 放在一组同时把狒狒题刷法与二分专题放在一组。时间久了你的知识树才会长出来。7.2 专题训练的重要性拿计算器系列举例“计算器”这个专题在 LeetCode 里很有意思224 无乘除但带括号和正负号227 无括号加乘除772 有括号又有乘除1 月 22 号这题则在 772 的基础上微调了嵌套逻辑。把它们放在一起刷你会看到同一套思路如何在不同约束条件下变形。我建议你做专题训练时至少连续刷同知识点 3 到 5 题。第一题熟悉套路第二题开始注意边界第三题能讲出思路第四题能闭着眼写模板。超过五题之后边际收益就开始下降可以换专题。7.3 周赛的复盘价值热词里出现的“LeetCode 周赛 430”说明最近很多人都在刷周赛。我的体会是周赛和每日一题是两种完全不同的训练方式。每日一题适合积累套路给你足够的时间慢慢打磨周赛则要求你在 90 分钟内完成四道题考的是快速定位题型和快速编码的能力。这两者不矛盾反而相辅相成。你在周赛里暴露出的节奏短板可以通过每日一题来补你在每日一题里学到的套路又会在周赛里帮你减少思考时间。复盘时我要你不要只盯着没做出来的题看。那些你用暴力解碰巧过了的题也值得重新做一遍最优解。因为“过了”不代表“会了”算法能力提升发生在从“过了”到“最优解”的这层训练里。8. 避坑清单与经验杂谈8.1 计算器题目速查表我整理了一个针对表达式求值类的快速检查清单按优先级排序检查点容易出现的问题建议的处理方式字符串去空格忘记过滤导致数字拼接错误在遍历循环开头统一跳过isspace()多位数拼接只取一位就入栈内层循环不断isdigit()结束后统一入栈括号匹配弹栈时顺序反转错误要reverse()后再拼接符号状态连续负号处理出错用sign变量逐层累积遇到括号临时保存除号整数除法Python 里//对负数向下取整先取绝对值整除再恢复符号嵌套深度递归解法可能爆栈显式用迭代栈替代递归8.2 狒狒题的避坑补充狒狒题除了二分边界和向上取整这两个经典坑之外还有一个很容易被忽略的小点当一堆香蕉的数量小于等于当前速度 V 时吃完这一堆也只算作一个小时。有些人会把它算成pile / V还纠正得很复杂其实只要你在代码里用(pile V - 1) // V一行就完全覆盖了这个逻辑。还有人在计算总时间时用sum(math.ceil(pile / V) for pile in piles)这个写法简洁但每次生成一个生成器对象如果数据量大会稍微慢一些。竞赛模板更推荐直接for循环累加常数小也不容易踩到浮点精度坑。math.ceil在整数运算里还更慢直接整数除法吧。8.3 我的几个“私人”习惯最后聊点多年来刷题积累的私人习惯。我把每日一题固定在早晨处理原因很简单早上的大脑相对清醒代码没写烦不容易因为情绪因素而跳过难题。如果某天题特别难我允许自己只写出思路草稿不做完整实现但第二天一定要补上。我还习惯给每个题建立一个 40 字的“一句话记忆点”。例如这道计算器变体的记忆点就是“遇左括号就存档遇右括号就结算反转”。这种高度压缩的表达配合少量复习能帮我快速建立索引。我也建议你准备一个专门的刷题笔记本不是让你抄代码而是记“昨天我的哪种思考方式导致失败”。这种记录比题解珍贵得多因为题解太多地方能看到但你自己的思维误区只存在于你自己的做题记录里。9. 实战体验总结回到开头那句话1 月 22 号的每日一题从难度、知识覆盖、可扩展性来说都是一道值得反复咀嚼的题。它让我重新审视了自己对栈的应用熟练度也让我意识到“会做一道题”和“能讲清楚一类题”之间的距离有多远。我个人的感受是LeetCode 的每日一题这个机制真正的价值并不在于让你一天学会一个新算法而在于它用很低的启动成本强迫你每天保持算法思维的连续性。今天这个题与括号纠缠明天那个题与二分纠缠一个月下来你会发现自己对很多原有知识点的理解都会发生质变。如果你还在犹豫要不要每天花这几十分钟去刷题我的建议很简单先定一个小的目标比如坚持完成本周的每日一题不做额外要求。等一周结束再评估自己的状态。大多数人的问题都是想得太远、做得太少而每日一题的设计天然治这个毛病——它把宏大目标切成了每天 30 分钟可完成的小块。最后再分享一个小技巧LeetCode 的每日一题没有强制绑定关系你完全可以在某天觉得题目过于简单时从热词榜里找一个同等难度的、自己薄弱环节的题目来替代刷。2026 年 1 月如果你觉得系统推荐不合口味后台还有整整几千道题等着被挖掘。重点不是你刷了哪道题而是你有没有把刷题当成一种每天都要做的、低成本的思维训练。习惯养成了剩下的就只是时间问题。