
如果你刷力扣刷到中等难度还没被“基本计算机”系列折磨过说明你运气不错。我第一次做力扣227题基本计算机Ⅱ的时候盯着32*2-6/3这个看起来人畜无害的表达式想了半天——不就是算个算术吗可真要把它写进代码里字符串解析、运算符优先级、边界条件一下就全涌上来了。这道题在算法面试里出镜率极高核心考的就是“栈 延迟结算”这套表达式求值思路。今天这篇文章不打算给你灌鸡汤就手把手把这题从思路到代码彻底讲透顺便捋一捋从227延伸到224、772、150这一整条表达式题目的打怪路线适合正在备战面试、或者想系统攻克栈专题的人。1. 先看题目这道题到底要我们做什么1.1 原题题面与示例题目描述非常简短给你一个字符串表达式s请你实现一个基本计算器来计算并返回它的值。整数除法仅保留整数部分向零截断。表达式只包含非负整数、加号、减号、乘号、除号和空格。几个示例直接感受一下s 32*2结果 7s 3/2 结果 1s 35 / 2 结果 5这里有三个隐含条件很重要第一输入只含非负整数所以不存在开头就是负号这种“经典陷阱”第二除法向零截断意味着-3/2的期望输出是-1而不是-2第三题目保证表达式合法不会出现连续运算符或除数为0的情况。这三句话读懂了后面很多坑基本可以绕开。为什么拿227而不是224来练手因为224带括号难度直接跳到困难而227恰好卡在“没有括号但存在优先级”的关键位置上。很多人以为括号才是难题其实真正麻烦的是“减号后面跟乘除”这种优先级纠缠。先打通227的无括号场景再去看224会发现括号只是多了一层递归外壳。1.2 为什么说这题考的是“优先级”而不是“计算”很多初学者第一反应是从左往右扫遇到乘除就先算不就行了这话说起来轻巧代码实现时全是坑。比如12*3如果你在扫描时遇到*再回头找之前的数字就要维护“上一个运算符是什么、上一个数字是多少、已经算到哪一段了”这一堆状态。状态一多代码就开始堆if-else越写越像打补丁最后自己都看不懂。出题人把括号去掉就是为了把“运算符优先级”这个核心矛盾单独抽出来考你。乘除优先级高加减优先级低它们混在一个表达式里时你不能简单地按从左到右顺序结算。解决思路其实很朴素既然加减法可以交换顺序、优先级最低那遇到加减法就先别急着算把数字“记账”存起来遇到乘除法优先级更高就必须当场算清楚再把结果存起来。等扫描完整个字符串把账本上所有数字加总就是答案。1.3 “暴力”在这里到底指什么刷题圈子里一提“暴力”大家首先想到的是暴力枚举——把所有运算顺序都试一遍复杂度直接爆炸。但227的“暴力”完全不是这个意思。它的暴力在于不建表达式树、不搞运算符优先级比较表、不递归下降就用一次遍历加一个栈把规则化解在每一次“遇到运算符”的瞬间。说白了这是一种“土办法干掉复杂模型”的思路土得有效土得优雅。你对比一下其他解法双栈记录数字和运算符、构建表达式树、递归下降解析器……这些方案不是说不好但面试现场五分钟内最稳妥、最不容易出错的就是栈 上一次运算符的延迟结算。这就是我标题里说的“暴力美学”——将复杂问题的解法抽到最简单的形态再用这个简单形态去面对所有边界情况。2. 暴力美学的核心心法延迟运算2.1 加减法可以“赖账”乘除法必须“当场清”我用一个生活化的类比来拆解这个套路。你把表达式当成一笔一笔的账单加号和减号是普通流水。遇到它们时你不需要立刻算总账只需要把“带符号的数字”记到账本上。比如遇到-3就在账本上记一条-3等月底统一结算。乘号和除号是加急订单。遇到它们必须立刻把账本上最新那条记录和当前这笔金额算清楚算完再放回账本。因为普通流水可以随便换顺序相加但加急订单如果拖到月底谁还记得它当初应该跟谁组合这个心态放在代码里非常直观。我们维护一个栈再维护一个变量op表示“上一次遇到的运算符”初值为。每次扫描到一个完整的数字时先不急着处理攒在num里。当遇到下一个运算符或者扫描到字符串末尾时才根据op决定怎么处理这个numop把num直接压入栈op-把-num压入栈op*弹出栈顶乘上num再压回栈op/弹出栈顶除以num再压回栈注意向零截断处理完之后把op更新成当前遇到的运算符num清零继续往下读。扫描完整个字符串后把栈里的数字全部加起来就是最终结果。2.2 手动推演一遍“32*2-6/3”的栈轨迹光说逻辑还是抽象我手动走一遍完整流程。表达式为s 32*2-6/3预期结果是5。初始化栈stack[]opnum0。读到3num3。读到一个触发结算op所以压入3。此时栈[3]更新opnum0。读到2num2。读到一个*触发结算op所以压入2。栈[3, 2]更新op*num0。读到2num2。读到-触发结算当前op*弹出栈顶2计算2*24压回。栈[3, 4]更新op-num0。读到6num6。读到/触发结算当前op-压入-6。栈[3, 4, -6]更新op/num0。读到3num3。字符串遍历结束触发结算当前op/弹出栈顶-6计算(-6)/3-2压回。栈[3, 4, -2]。最终求和34(-2)5。整个过程里数字只入栈一次乘除法在入栈前完成结算优先级问题被自然化解。这就是整套解法的核心你可以在纸上把这个推演过程写出来比空想代码高效得多。2.3 这个解法暴在哪里又美在哪里“暴力”在这题里不是贬义。它暴在“不回避数字、不提前设计运算符优先级状态机”而是把优先级规则压缩到“遇到运算符的那一瞬间”处理。每一段以乘除为单位的表达式在入栈前就已经被算成一个整体栈里最终装的全是“可以直接相加的带符号整数”。“美”在最后那一步把所有栈内数字累加。这一步相当于把四种运算统一成了加法。乘除法先把结果算好压栈压下去的都是可以直接相加的单独项整个表达式被拆成一组带符号整数。这种化简思维在算法里非常常见——逆波兰表达式求值、表达式树构建本质上都是同一招。3. 直接抄作业三种语言的完整实现3.1 C版本最贴近面试场景面试中用C写这类题目很常见代码干净利落class Solution { public: int calculate(string s) { stackint st; int num 0; char op ; for (int i 0; i s.size(); i) { char c s[i]; if (isdigit(c)) { num num * 10 (c - 0); } // 当前字符是运算符或者到了字符串末尾 if ((!isdigit(c) c ! ) || i s.size() - 1) { if (op ) { st.push(num); } else if (op -) { st.push(-num); } else if (op *) { int top st.top(); st.pop(); st.push(top * num); } else if (op /) { int top st.top(); st.pop(); st.push(top / num); } op c; num 0; } } int ans 0; while (!st.empty()) { ans st.top(); st.pop(); } return ans; } };几个值得注意的细节代码里的num num * 10 (c - 0)负责处理多位数这是表达式解析的基本功漏掉它面对123就会错。结算条件是(!isdigit(c) c ! ) || i s.size() - 1既过滤了空格又保证了最后一个数字不会漏算。op存的是“上一次遇到的运算符”所以遇到乘号时实际上是用之前的op处理之前攒下的num之后再把op更新为当前字符。这个先后顺序第一次写很容易弄反。栈的类型我用int足够通过全部用例如果想更保险用long long也完全没问题因为中间乘法结果可能比最终答案更大。3.2 Java版本与Python版本的差异点Java版本的核心逻辑与C完全一致只有两处语法差异字符判断用Character.isDigit(c)栈改用DequeInteger。完整实现如下class Solution { public int calculate(String s) { DequeInteger stack new ArrayDeque(); int num 0; char op ; for (int i 0; i s.length(); i) { char c s.charAt(i); if (Character.isDigit(c)) { num num * 10 (c - 0); } if ((!Character.isDigit(c) c ! ) || i s.length() - 1) { if (op ) { stack.push(num); } else if (op -) { stack.push(-num); } else if (op *) { int top stack.pop(); stack.push(top * num); } else if (op /) { int top stack.pop(); stack.push(top / num); } op c; num 0; } } int ans 0; for (int v : stack) { ans v; } return ans; } }Python版本有个最关键的坑除法向零截断。Python的//是向下取整而题目要求向零截断所以不能直接写top // num。正确做法是用int(top / num)利用浮点除法再截断或者显式调用math.trunc(top / num)。完整实现如下class Solution: def calculate(self, s: str) - int: stack [] num 0 op s s.replace( , ) # 提前去掉空格让循环更清爽 for i, c in enumerate(s): if c.isdigit(): num num * 10 int(c) if not c.isdigit() or i len(s) - 1: if op : stack.append(num) elif op -: stack.append(-num) elif op *: stack.append(stack.pop() * num) elif op /: stack.append(int(stack.pop() / num)) # 注意向零截断 op c num 0 return sum(stack)Python里我用s.replace( , )提前清掉空格这样循环里不用每次都判断c ! 可读性更好。sum(stack)一步把栈内所有数字加总非常契合“最后统一结算”的思路。3.3 复杂度分析与极端边界论证时间复杂度是 O(n)每个字符只被扫描一次栈操作总次数不会超过字符串长度。空间复杂度是 O(n)最坏情况下表达式全是加减法所有数字都会压栈比如12-34-5...。边界情况建议这样测纯数字123循环走到最后一定触发i len-1分支按op压入123结果正确。字符串首尾都有空格的 35 空格不会触发结算中间运算符正常处理结果正确。表达式合法所以栈不会为空时弹出num0不会造成除零。这三类边界覆盖完代码基本就稳了。4. 我踩过的坑希望你一个都别踩4.1 多位数解析与空格过滤我第一次写这题犯的错非常低级只处理了个位数。当时把num num * 10 (c - 0)写成了num c - 0结果面对1231直接算出2。这种错误往往还不容易一眼看出来因为示例通常给的是小数字直到你用123这种用例才会露馅。记住在遇到运算符之前数字可能很长必须把每一位累加进num。空格的处理也要小心。有人习惯在循环开头单独写if (c ) continue;这没问题但注意别把空格当成运算符去触发结算。我最推荐的写法是像上面那样把空格过滤合并进结算条件(!isdigit(c) c ! )这样逻辑最紧凑不会多出分支。4.2 末尾数字的最后一次结算这是几乎所有新手都会漏掉的一点循环结束后最后一个数字还没有被处理过。有人会在循环外再写一遍if (op ) ... else if ...代码瞬间重复冗长还容易抄错。更好的做法有两种。第一种是在循环条件里加上|| i s.size() - 1让扫描到末尾时强制触发一次结算。第二种是给字符串末尾补一个不会出现的哨兵运算符比如s #遇到#时同样触发结算。两种都能避免“写完循环再补一段重复代码”的尴尬面试时用第二种还会让面试官觉得你懂收尾技巧。4.3 整数除法向零截断C/Java/Python天差地别这是227题最容易翻车的地方而且不会在最初的测试用例里撞上。语言整数除法的舍入行为227题的正确写法C向零截断top / numJava向零截断top / numPython//向下取整int(top / num)C和Java的/天生向零截断-3/2得到-1正好符合题意。Python的//是地板除-3//2得到-2直接违反题意。很多刷题新手从网上复制题解时没注意这一点本地跑负用例才发现不对。为什么差这么多本质是不同语言对“整数除法如何舍入”的定义不同。写算法题千万别默认所有语言行为一致这就是血泪教训。4.4 符号与栈顶配合的隐藏细节除法结算时要注意栈顶的符号。比如表达式0-6/3扫描到-时-6会被当作一个整体压进栈栈是[0, -6]。等遇到/时弹出的是-6计算(-6)/3得到-2最终结果是0 (-2) -2。这里如果理解成“先算6/3再取负”结果虽然相同但思维路径是错的碰上0-6/ -3这类带隐含负号的情况就会乱。另一个细节是op的初始化。它恰好覆盖了表达式以数字开头的场景第一个数字直接按加法入栈不需要为“表达式开头”单独写分支。如果你把初始值设成\0或 前面就要多写判断逻辑既丑又容易出错。提示op初始化为是整个解法的点睛之笔它让第一个数字天然按正数入栈。面试时主动提到这一点通常能让面试官觉得你对代码细节有把控力。5. 面试加分项从“能过题”到“能说服面试官”5.1 讲思路的五步话术模板面试不是闷头写代码。我强烈建议按下面这个顺序给面试官讲思路而不是上来就敲键盘第一步先把问题简化这个表达式没有括号只有四则运算核心矛盾是运算符优先级。 第二步提出关键观察加法和减法优先级最低可以最后统一结算乘法和除法优先级高必须在遇到时立刻算。 第三步说明数据结构用一个栈把加减法的数字以带符号形式压栈用一个op变量记录上一个运算符。 第四步讲清触发时机每遇到一个运算符或者扫描到末尾就用op和当前数字更新栈然后更新op、清零num。 第五步说收尾最后把栈内所有数字相加表达式被拆成一组可加项复杂度O(n)。这五步讲完面试官基本就不会再刁难主思路了。很多候选人一上来就写代码写完才解释表达效果差很多因为面试官在你写代码的几分钟里其实没法完全跟上你的心路历程。5.2 面试官最爱的三个追问与应对第一个追问如果有括号怎么办比如224题。正确回答是括号代表子表达式的边界可以用递归或辅助栈。递归时遇到(就调用同一个函数解析括号内内容返回值当成一个“数字”继续走主流程。本质就是把子表达式结果塞回同一个结算循环。第二个追问能不能不用栈做到O(1)空间这个一般面试不会硬卡但有备无患。思路是只保留“上一个需要和乘除结合的数值”prev遇到加减法时更新结果并重置prev遇到乘除法时更新prev最后把结果和prev相加。代码比栈写法更容易出错不建议面试时优先提。第三个追问如果表达式里有一元负号比如1- -2或-12怎么办227题明确只含非负整数所以不用考虑。但你可以主动补充“如果出现负数我可以在读取数字时增加一个符号位判断或者在进入主循环前对字符串做预处理。”这种主动延伸能体现你的思考深度。6. 这套暴力美学能打多少怪表达式类题目的家族图谱6.1 从227到224有括号之后怎么办224题基本计算器I带括号和加减法没有乘除乍一看更简单实际因为括号的出现难度比227更别扭。常见解法是递归遇到(就去递归解析括号内的表达式返回一个值当数字遇到右括号就结束当前递归层。也可以用双栈法一个栈存数字、一个栈存运算符遇到)时不断弹出运算符计算直到遇到(。我建议的学习顺序是先把227的迭代 栈解法吃透再用递归去解224。很多人先做224会觉得混乱是因为还没掌握延迟结算这把钥匙。227练熟了224的递归外壳基本等于白送。6.2 从227到150逆波兰式的降维打击150题逆波兰表达式求值给的是后缀表达式例如[2,1,,3,*]优先级已经帮你排好了。解法极度简单遇到数字入栈遇到运算符弹出两个数字计算结果再入栈最后栈顶就是答案。你细品一下这跟227的栈解法是一脉相承的。227其实可以理解成“一边解析中缀表达式、一边把它转成逆波兰式并计算”的降维版本。理解了227150基本不用花时间刷。同样的道理KMP、归并排序那些经典模板当然要练但面试现场能救你命的往往是这类看似简单、实则综合的题。6.3 一个完整的刷题路线建议如果你想系统搞定栈和表达式相关题目推荐按这条路线走顺序题号题型核心考点建议120有效的括号栈匹配热身题建立栈直觉2150逆波兰表达式求值栈 运算顺着做难度低3227基本计算器II优先级 延迟结算重点练本文核心4224基本计算器I递归 / 双栈进阶吃透括号5772基本计算器III括号 乘除终极挑战综合题这条曲线走完表达式求值这类题对你来说就是模板题。刷题攻略里也常把227列为栈专题必刷因为它覆盖的知识点——字符串解析、运算符优先级、栈操作、边界处理——几乎是所有解析型题目的公共底座。最后说一点我自己的刷题体会。227这道题我第一次做的时候代码写得又臭又长原因就是总想着“遇到乘号就回头处理之前的数字”结果状态变量越加越多。后来把op这个概念反复写了几遍才真正体会到“记住上一次运算符”这个视角的妙处——你不需要往前看只需要记住上一站。之后再碰到224、772我基本就是同一套代码加个递归外壳。如果你正在备考我建议拿到题先别搜题解自己拿笔手动模拟三个用例把栈的每一步变化写出来。一个小技巧是先用“纯数字 一个运算符”的短例跑通逻辑再补多位数和空格这两个边界AC速度会肉眼可见地提升。别问我怎么知道的都是泪。