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 1:
s = "3+2*2"→ 输出7 - Example 2:
s = " 3/2 "→ 输出1 - Example 3:
s = " 3+5 / 2 "→ 输出5
约束条件(来自原题文档):
1 <= s.length <= 3 * 10^5s由整数和运算符('+', '-', '*', '/')组成,运算符之间以任意数量的空格分隔s表示一个合法表达式- 表达式中所有整数均为
[0, 2^31 - 1]范围内的非负整数 - 答案保证能够放入一个 32 位整数
值得注意的两个细节:输入表达式中不存在负数常量(负数通过-运算符作用于后续操作数产生);整数除法仅保留整数部分,例如3/2 = 1,这与多数编程语言中整数除法向零截断的语义一致,意味着-3/2这类除法同样向零截断。
核心解题思路:先乘除、后加减
原文档给出了非常清晰的思路主线:
这道题是第 224 题的加强版。第 224 题中只有加减运算和括号,这一题增加了乘除运算。由于乘除运算的优先级高于加减,所以先计算乘除运算,将算出来的结果再替换回原来的算式中。最后只剩下加减运算,于是题目降级成了第 224 题。
具体算法如下:
- 把加减运算符号后面的数字压入栈中;
- 遇到乘除运算,直接将它与栈顶的元素计算,并将计算后的结果放回栈顶;
- 若读到一个运算符,或者遍历到字符串末尾,即认为是遍历到了数字末尾;
- 处理完该数字后,更新
preSign为当前遍历的字符; - 遍历完字符串
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),两个条件触发结算:
- 遇到运算符(非数字且非空格)——说明当前数字已完整读取;
- 遍历到字符串末尾——最后一个数字后面没有运算符,必须强制结算。
空格既不参与数字拼接,也不触发结算,只被跳过。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边界情况与易错点总结
- 整数除法向零截断:Go 的
/运算符对整数本身就向零截断,与题目语义一致,无需额外处理。但若自行实现时使用math.Floor等浮点手段会得到-1而非0(对-5/6而言),必须避免; - 减法压入负数:
-后面紧跟的数字以负数形式入栈,保证了最终求和逻辑的统一,也使得"2-1+2"这类表达式天然正确; - 末尾数字强制结算:
i == len(s)-1分支不可或缺,否则最后一个数字永远不会进入栈中; - preSign 初始化:必须为
'+',否则表达式首项无法入栈; - 空格处理:空格既不影响数字拼接也不触发结算,仅被
ch != ' '条件过滤,支持任意数量空格; - 大输入规模:
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),仅供参考