LeetCode 772. 基本计算器 III
题目描述
题意分析
计算一个包含整数、加减乘除、括号和空格的合法表达式。括号内先算,同一层中乘除优先于加减,同优先级运算按从左到右的顺序计算。
整数字面量按非负数读取,但减法和括号内计算可以得到负数。整数除法向零截断;只需返回计算结果,不需要保留表达式或调用通用表达式求值工具。
解法:递归处理括号 + 两级累计
核心思路
[!blue]
把一层表达式看成若干个用加减连接的乘除项。若读到一个数字就立刻把所有运算混在一起计算,后面出现乘除时便无法只修改它所属的项。因此分别维护
sum和term:sum保存已经结束的项之和,term保存当前尚可能继续乘除的带符号项。
op记录当前操作数前面的运算符。读完一个完整操作数num后,按它更新状态:
- 前置符号为
+:把旧term加入sum,以num开始新项。- 前置符号为
-:同样结算旧项,以-num开始新项,把减法体现在项的符号中。- 前置符号为
*或/:只更新当前term,不影响此前已经结束的项。每次应用完整操作数后,
sum + term就是本层当前已读部分的值。乘除始终留在当前项中,并按扫描顺序执行,所以同时满足优先级和从左到右的结合顺序。初始op = '+'、sum = term = 0,使首个操作数也能使用相同规则。括号只负责产生一个完整操作数:遇到
(就递归计算其中表达式,将返回值作为当前层的num,再应用当前层的op。每层拥有自己的sum、term和op,因此内层加减不会提前结算外层的乘除项。扫描位置需要在递归层之间连续传递。Java 用共享的一元素数组保存下标,Go 同时返回计算值和下一下标;内层负责消费自己的
),父层从括号后继续读取。遇到当前层右括号或字符串末尾时结束,返回sum + term,把最后一个尚未结算的项一并计入。
解题步骤
- 从下标零开始解析,初始化本层
sum = 0、term = 0、op = '+'。- 跳过空格,连续读取数字组成一个整数;若遇到左括号,则递归取得括号内结果。
- 根据操作数前的
op更新状态:加减结束旧项并开启新项,乘除直接修改当前项。- 跳过空格并读取下一个运算符;遇到当前层右括号或末尾则结束。
- 当前层消费自己的右括号,返回
sum + term,顶层返回最终结果。
代码实现
class Solution {
public int calculate(String s) {
return parse(s, new int[1]);
}
private int parse(String s, int[] index) {
int sum = 0;
int term = 0;
char op = '+';
while (index[0] < s.length()) {
while (index[0] < s.length() && s.charAt(index[0]) == ' ') {
index[0]++;
}
if (index[0] == s.length() || s.charAt(index[0]) == ')') {
break;
}
int num = 0;
if (s.charAt(index[0]) == '(') {
index[0]++;
// 括号内先算完,返回值作为当前层的一个完整操作数。
num = parse(s, index);
} else {
while (index[0] < s.length()
&& s.charAt(index[0]) >= '0'
&& s.charAt(index[0]) <= '9') {
num = num * 10 + s.charAt(index[0]++) - '0';
}
}
// 加减封口旧项,乘除继续更新当前项,保留运算优先级。
if (op == '+') {
sum += term;
term = num;
} else if (op == '-') {
sum += term;
term = -num;
} else if (op == '*') {
term *= num;
} else {
term /= num;
}
while (index[0] < s.length() && s.charAt(index[0]) == ' ') {
index[0]++;
}
if (index[0] < s.length() && s.charAt(index[0]) != ')') {
op = s.charAt(index[0]++);
}
}
if (index[0] < s.length() && s.charAt(index[0]) == ')') {
index[0]++;
}
// 最后一个乘除项尚未封口,返回时也必须计入。
return sum + term;
}
}
func calculate(s string) int {
value, _ := parseExpression(s, 0)
return value
}
func parseExpression(s string, index int) (int, int) {
sum, term := 0, 0
op := byte('+')
for index < len(s) {
for index < len(s) && s[index] == ' ' {
index++
}
if index == len(s) || s[index] == ')' {
break
}
num := 0
if s[index] == '(' {
index++
// 括号内先算完,并把已经消费右括号后的下标交回当前层。
num, index = parseExpression(s, index)
} else {
for index < len(s) && s[index] >= '0' && s[index] <= '9' {
num = num*10 + int(s[index]-'0')
index++
}
}
// 加减封口旧项,乘除继续更新当前项,保留运算优先级。
switch op {
case '+':
sum += term
term = num
case '-':
sum += term
term = -num
case '*':
term *= num
case '/':
term /= num
}
for index < len(s) && s[index] == ' ' {
index++
}
if index < len(s) && s[index] != ')' {
op = s[index]
index++
}
}
if index < len(s) && s[index] == ')' {
index++
}
// 最后一个乘除项尚未封口,返回时也必须计入。
return sum + term, index
}
复杂度分析
设字符串长度为 $n$,括号最大嵌套深度为 $h$。
- 时间复杂度:$O(n)$。扫描位置始终向右,各层合计只读取每个字符常数次,不会重新扫描整个括号区间。
- 辅助空间复杂度:$O(h+1)$,每层保存常数状态,顶层也占一个调用。
关键点总结
[!green]
- 括号递归解决嵌套,
sum与term分开解决同层优先级。op属于当前操作数之前,只有完整读出操作数后才执行它。- 每层消费自己的右括号,并把更新后的扫描位置交还父层。
- 最后一项还在
term中,返回时必须加上它。
易错点总结
[!yellow]
- 乘除直接修改总和,会把前面已经结束的加减项也卷进运算,破坏优先级。
- 减法对应的当前项应保存为负值,后续乘除继续作用于这个带符号项。
- 只在读到后续运算符时结算,容易漏掉最后一个操作数或最后一项。
- 递归不共享或返回扫描位置,会重复读取内层内容;不消费自己的右括号,又会让父层提前结束。
- 多位数按
num = num * 10 + digit累积,不能把每个字符都当成独立操作数。- Java、Go 的整数除法都向零截断,不能改为对负数向下取整。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 224. 基本计算器 | 困难 | 复用括号与加减解析,再补充乘除优先级。 |
| 227. 基本计算器 II | 中等 | 复用乘除优先级处理,再增加括号嵌套与递归返回。 |
| 394. 字符串解码 | 中等 | 用栈保存嵌套表达式的中间状态;本题同时处理括号与运算优先级,该题展开带重复次数的嵌套片段。 |
| 770. 基本计算器 IV | 困难 | 计算器系列,复用表达式解析与运算优先级。IV 保留未替换变量,将中间结果表示为多项式。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!