LeetCode-Go 题解:227. Basic Calculator II 单栈一次遍历实现四则混合运算求值
2026/9/10 0:16:30 网站建设 项目流程

LeetCode-Go 题解:227. Basic Calculator II 单栈一次遍历实现四则混合运算求值

【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go

本文基于 LeetCode-Go 仓库中 leetcode/0227.Basic-Calculator-II/README.md 展开,深入剖析「Basic Calculator II」这道经典表达式的求值题在 Go 中的栈实现:利用单个栈与前置运算符(preSign)思想,先处理乘除、再合并加减,一次遍历即可完成含空格的四则混合运算求值。读完本文,你将掌握这套可复用的单栈求值模板,并理解其与第 224 题(含括号版计算器)之间的递进关系。

题目概述与约束

给定一个字符串表达式s,计算并返回它的值,其中整数除法向零截断(只保留整数部分)。

  • Example 1s = "3+2*2"→ 输出7
  • Example 2s = " 3/2 "→ 输出1
  • Example 3s = " 3+5 / 2 "→ 输出5

约束条件(来自原题文档):

  • 1 <= s.length <= 3 * 10^5
  • s由整数和运算符('+', '-', '*', '/')组成,运算符之间以任意数量的空格分隔
  • s表示一个合法表达式
  • 表达式中所有整数均为[0, 2^31 - 1]范围内的非负整数
  • 答案保证能够放入一个 32 位整数

值得注意的两个细节:输入表达式中不存在负数常量(负数通过-运算符作用于后续操作数产生);整数除法仅保留整数部分,例如3/2 = 1,这与多数编程语言中整数除法向零截断的语义一致,意味着-3/2这类除法同样向零截断。

核心解题思路:先乘除、后加减

原文档给出了非常清晰的思路主线:

这道题是第 224 题的加强版。第 224 题中只有加减运算和括号,这一题增加了乘除运算。由于乘除运算的优先级高于加减,所以先计算乘除运算,将算出来的结果再替换回原来的算式中。最后只剩下加减运算,于是题目降级成了第 224 题。

具体算法如下:

  1. 把加减运算符号后面的数字压入栈中;
  2. 遇到乘除运算,直接将它与栈顶的元素计算,并将计算后的结果放回栈顶;
  3. 若读到一个运算符,或者遍历到字符串末尾,即认为是遍历到了数字末尾;
  4. 处理完该数字后,更新preSign为当前遍历的字符;
  5. 遍历完字符串s后,将栈中元素累加,即为该字符串表达式的值。

这种做法的巧妙之处在于:用「延迟结算」消除优先级问题。遇到+/-时并不立即计算,而是把带符号的数字压栈(减法压入负数);遇到*//时立即与栈顶元素结算,因为乘除的优先级最高,此时结算不会影响后续结果。最终栈中只剩下一串正负整数,求和即可。

时间复杂度 O(n),空间复杂度 O(n),其中 n 为字符串长度(栈中最多存储 O(n) 个操作数)。

Go 源码逐行解析

仓库中的完整实现位于 leetcode/0227.Basic-Calculator-II/227. Basic Calculator II.go,核心函数如下:

func calculate(s string) int { stack, preSign, num, res := []int{}, '+', 0, 0 for i, ch := range s { isDigit := '0' <= ch && ch <= '9' if isDigit { num = num*10 + int(ch-'0') } if !isDigit && ch != ' ' || i == len(s)-1 { switch preSign { case '+': stack = append(stack, num) case '-': stack = append(stack, -num) case '*': stack[len(stack)-1] *= num default: stack[len(stack)-1] /= num } preSign = ch num = 0 } } for _, v := range stack { res += v } return res }

逐行拆解其状态机逻辑:

状态变量初始化

  • stack[]int{}动态切片栈,用于暂存待累加的带符号操作数;
  • preSign:记录「当前数字之前的运算符」,初始化为'+'。这个初始值非常关键——表达式第一个数字前没有显式符号,但按加法压栈恰好等价于「首项为正」,省去了首元素特判;
  • num:正在拼接的当前数字(连续数字字符按十进制累乘);
  • res:最终结果累加器。

数字拼接阶段

if isDigit { num = num*10 + int(ch-'0') }

当字符为数字时,把num左移一位(乘 10)再叠加当前位,完成多位整数的解析,例如"52"会被解析为5*10 + 2 = 52

结算触发条件

if !isDigit && ch != ' ' || i == len(s)-1 {

这是一个值得注意的复合条件,等价于(!isDigit && ch != ' ') || (i == len(s)-1),两个条件触发结算:

  1. 遇到运算符(非数字且非空格)——说明当前数字已完整读取;
  2. 遍历到字符串末尾——最后一个数字后面没有运算符,必须强制结算。

空格既不参与数字拼接,也不触发结算,只被跳过。range遍历得到的是i(字节索引)与ch(rune),由于题目保证输入只含 ASCII 字符(数字、运算符、空格),字节索引即字符索引,i == len(s)-1的判断是正确的。

按 preSign 分派结算

switch preSign { case '+': stack = append(stack, num) case '-': stack = append(stack, -num) case '*': stack[len(stack)-1] *= num default: stack[len(stack)-1] /= num }
  • +:把num原样压栈;
  • -:把-num压栈(转化为负数,后续统一求和);
  • *stack栈顶元素原地乘num
  • default(即/):栈顶元素原地除以num(Go 整数除法即向零截断,天然满足题目要求)。

注意preSign数字之前的运算符:例如表达式3+2*2,读到+时触发结算,此时preSign仍是初始值'+',把3压栈;随后更新preSign = '+';读到*时触发结算,把2压栈,更新preSign = '*';读到末尾2时按'*'执行stack[len(stack)-1] *= 2,栈顶由2变为4。最终栈为[3, 4],求和得7

结算后的状态推进

preSign = ch num = 0

更新前置运算符为当前字符,并清零num开始解析下一个数字。

结果汇总

for _, v := range stack { res += v } return res

最后把栈中所有元素累加。由于栈中只保存带符号的操作数(乘除已在入栈/栈顶结算阶段完成),这里的累加即是最终答案。

与 224 题 Basic Calculator 的对比与降级关系

原文档明确指出本题是 第 224 题 Basic Calculator 的加强版:224 题只有加减与括号,227 题加入了乘除但去掉了括号。因此 227 题的解法先把乘除结算掉,把表达式「降级」为纯加减问题,而这正是 224 题已经解决的问题形态。

对比仓库中 224 题的实现 leetcode/0224.Basic-Calculator/224. Basic Calculator.go 可以发现两者的差异与联系:

  • 224 题calculate(s string)使用container/list作为栈,处理+-()四种符号。遇到(时把「当前结果 result」与「符号状态 sign」压栈,进入括号内的新计算域;遇到)时按result * sign + 之前结果弹出恢复。它不需要 preSign 机制,因为加减可以直接累加进result
  • 227 题:没有括号,但引入乘除后不能再即时累加,必须用preSign延迟结算,把加减数字入栈、乘除数字与栈顶合并。

两题的共同点是都基于「栈」这一数据结构做运算符优先级管理。可以说,掌握了 224 的括号处理与 227 的乘除 preSign 机制,就覆盖了 LeetCode「基本计算器」系列(224、227、772 等)中最核心的两类优先级处理手段。

测试用例验证

仓库为该题编写了完整的测试,位于 leetcode/0227.Basic-Calculator-II/227. Basic Calculator II_test.go,测试结构遵循仓库统一的question227/para227/ans227表驱动风格:

输入期望输出覆盖点
"3+2*2"7乘号优先级高于加号
"3/2"1整数除法向零截断
" 3+5 / 2 "5运算符两侧带空格
"1 + 1"2空格与加法
" 2-1 + 2 "3减法与混合空格
"2-5/6"2减法与除法混合(2 - 0 = 2

其中"2-5/6"是很有代表性的边界用例:5/6向零截断为0,因此表达式等价于2-0 = 2,验证了整数除法截断与减法压栈(-5先入栈再被除法原地结算为-0)的正确性。

运行测试的方式与仓库其他题目一致。仓库根目录的 gotest.sh 展示了全量测试命令:

go test -covermode=atomic -coverprofile=coverage.txt ./leetcode/...

若只需运行本题测试,可进入对应目录执行:

cd leetcode/0227.Basic-Calculator-II && go test -v

边界情况与易错点总结

  1. 整数除法向零截断:Go 的/运算符对整数本身就向零截断,与题目语义一致,无需额外处理。但若自行实现时使用math.Floor等浮点手段会得到-1而非0(对-5/6而言),必须避免;
  2. 减法压入负数-后面紧跟的数字以负数形式入栈,保证了最终求和逻辑的统一,也使得"2-1+2"这类表达式天然正确;
  3. 末尾数字强制结算i == len(s)-1分支不可或缺,否则最后一个数字永远不会进入栈中;
  4. preSign 初始化:必须为'+',否则表达式首项无法入栈;
  5. 空格处理:空格既不影响数字拼接也不触发结算,仅被ch != ' '条件过滤,支持任意数量空格;
  6. 大输入规模s.length可达3 * 10^5,单次线性遍历 + 栈操作均为 O(1) 均摊,可以轻松应对该规模;答案保证在 32 位整数范围内,Go 的int在 64 位平台上为 64 位,不会溢出。

小结

LeetCode-Go 仓库对 227 题给出的解法是一个极简而优雅的单栈模板:一个栈、一个前置运算符、一次遍历。它把「优先级」问题转化为「结算时机」问题——低优先级的加减延迟入栈,高优先级的乘除立即结算,最终栈内只余待求和的带符号整数。这套思路不仅适用于本题,也是处理无括号四则表达式求值的通用范式,与仓库中 224 题的括号栈解法形成互补,共同构成「Basic Calculator」系列的两块基石。

参考实现与测试:

  • 解题源码
  • 单元测试
  • 原题文档
  • 224 题括号版解法
  • 仓库通用数据结构(含 Stack)

【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询