基本计算器为什么常用栈处理括号和符号?
简化版
只含 +、-、括号和整数的基本计算器,可以维护当前结果 res、当前符号 sign 和当前数字 num。遇到 ( 时把外层 res 和 sign 入栈并重置;遇到 ) 时先结算当前数字,再用弹出的符号和外层结果合并。
详细版
括号会开启一段新的局部表达式。进入括号前,外层已经累积的结果和括号前的符号必须保存;括号结束后,把括号内结果当成一个整体数字合并回外层。
int calculate(String s) {
Deque<Integer> stack = new ArrayDeque<>();
int res = 0, sign = 1, num = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
num = num * 10 + (c - '0');
} else if (c == '+') {
res += sign * num; num = 0; sign = 1;
} else if (c == '-') {
res += sign * num; num = 0; sign = -1;
} else if (c == '(') {
stack.push(res); stack.push(sign);
res = 0; sign = 1;
} else if (c == ')') {
res += sign * num; num = 0;
res = stack.pop() * res + stack.pop();
}
}
return res + sign * num;
}
空格直接跳过。多位数字要累积,最后一个数字也要在循环结束后结算。
完整版教学
一、表达式求值的难点在哪里
对于只有加减法的表达式,运算优先级本身不复杂,真正麻烦的是括号。括号会让一段子表达式先算,并且括号前的符号会作用到整个括号结果。例如 1-(2+3) 不是 1-2+3,而是 1 - 5 = -4。
1 - (2 + 3)
外层: 已有 res=1, 括号前 sign=-1
内层: 2+3=5
合并: 1 + (-1)*5 = -4
栈要保存的就是进入括号前的外层现场。
二、三个变量分别代表什么
res 表示当前层已经结算的结果,sign 表示当前数字前面的符号,num 表示正在读取的多位数字。遇到操作符时,把 sign * num 加到 res,然后重置数字并更新符号。
| 变量 | 含义 | 示例 |
|---|---|---|
res | 当前括号层已结算结果 | 读完 1+2 后是 3 |
sign | 当前数字的正负 | 读到 - 后变成 -1 |
num | 正在累积的数字 | 读 123 时依次变 1、12、123 |
这样不需要等整条表达式结束才处理加减。
三、遇到左括号为什么入栈
左括号表示进入新层。进入前,外层的 res 和括号前的 sign 都必须保存,因为括号内会重新从 0 开始计算。入栈顺序可以是先 res 后 sign,出栈时反过来使用。
表达式: 8-(3-1)
遇到 '(' 前: res=8, sign=-1
入栈: [8, -1]
括号内重新计算: 3-1=2
合并: 8 + (-1)*2 = 6
这个“保存现场 -> 新层计算 -> 恢复现场”就是栈的典型用途。
四、右括号如何合并结果
遇到 ) 时,当前层最后一个数字可能还没结算,所以先执行 res += sign * num。然后弹出括号前的符号 outerSign 和外层结果 outerRes,把当前层结果当成一个整体数字:
res = outerRes + outerSign * innerRes
如果入栈时用了 stack.push(res); stack.push(sign);,那么出栈时先弹到的是 sign,再弹到的是 res。顺序写错会让结果完全偏掉。
五、和逆波兰表达式有什么不同
逆波兰表达式已经把优先级体现在 token 顺序里,求值时遇到操作符弹两个数即可。基本计算器面对的是中缀表达式,必须在扫描时处理括号和符号上下文。
| 问题 | 栈里保存 | 触发点 |
|---|---|---|
| 逆波兰表达式 | 数字操作数 | 遇到操作符 |
| 基本计算器 | 外层结果和符号 | 遇到括号 |
如果题目加入 *、/,还要处理乘除优先级,常见做法是用栈保存带符号项,或用表达式解析框架。
六、常见误区与追问
记忆钩子:左括号是“存档开新局”,右括号是“结算本局,再乘上存档里的符号合回去”。
- 误区:括号前的负号只影响括号里第一个数字。 它影响整个括号结果,例如
-(2+3)是 -5。 - 误区:遇到
)直接合并,忘记先结算num。 当前层最后一个数字还在num中,必须先加入res。 - 误区:多位数字按单个字符处理。
123要累积成一个数字,不能当作 1、2、3 分别计算。 - 追问:空格怎么处理? 空格不是数字、符号或括号,跳过即可。
- 追问:为什么最后还要
res + sign * num? 表达式可能以数字结尾,最后一个数字没有被操作符触发结算。 - 追问:如果有乘除怎么扩展? 需要额外处理优先级,通常用操作数栈/操作符栈,或把乘除立即合并为一个项。
七、加强记忆
基本计算器的主线是“当前层结算 + 括号层存档”。res/sign/num 负责没有括号时的线性加减;栈负责在 ( 时保存外层结果和符号,在 ) 时把内层结果作为整体合并回去。抓住“括号结果整体受前一个符号影响”,这题就能写清楚。