目录

题目描述

224. 基本计算器

题意分析

输入是一个合法的表达式字符串,只包含非负整数、+-、括号和空格,要求算出它的值。约束里有两个重要信号:一是没有乘除,所有运算优先级相同,只有括号会改变计算顺序;二是 - 可能是一元负号,比如 "-(2+3)""-2+1",负号前面没有左操作数。

边界上还要留意:数字可能是多位数("123" 要拼成一个整数而不是三个);空格可以出现在任何位置,必须被无视;括号可以嵌套任意多层,如 "(1+(4+5+2)-3)"

也就是说,难点不在「算加减」,而在「括号改变了外层与内层的关系」以及「负号不一定是减法」这两件事上。

解法:栈保存括号外上下文

核心思路

问题关键:表达式只有加减,优先级本身不难;难点是多位数、嵌套括号和一元负号。反复寻找最内层括号并替换字符串会产生重复扫描,最坏达到 $O(n^2)$。

为什么选栈:扫描到 ( 时,当前层还没算完,只需保存“括号外已累计的结果”和“括号前的符号”;扫描到 ) 时恢复这两个值即可。因为只有加减,括号整体只会以 +1-1 的系数并回外层,不需要通用的运算符优先级栈。

状态定义与不变量result 表示当前括号层已结算的值,num 表示正在读取的数字,sign 表示 num 前的符号。任意扫描位置,result + sign × num 就是当前层已读取部分的值;栈中按层保存每个未闭合括号的 (outerResult, outerSign)

遇到左括号时压入外层状态,并从 result = 0、sign = 1 开始计算内层;遇到右括号时先结算内层最后一个数,再计算 outerResult + outerSign × innerResult。表达式开头或左括号后的 - 可视为 0 - x,无需单独分支。

解题步骤

  1. 初始化 result = 0、num = 0、sign = 1,从左到右扫描。
  2. 遇到数字,用 num = num × 10 + digit 拼接多位数。
  3. 遇到 +-,先把 sign × num 加入 result,清空 num,再更新下一个数字的符号。
  4. 遇到 (,依次保存 resultsign,然后重置当前层状态。
  5. 遇到 ),先结算内层最后一个数字,再弹出符号和外层结果,合并为 outerResult + outerSign × innerResult
  6. 空格直接忽略;扫描结束后补结算最后一个数字。

例如 1-(2-3):进入括号前保存 (1,-1),内层算得 -1,出括号后合并为 1 + (-1) × (-1) = 2

代码实现

class Solution {
    public int calculate(String s) {
        Deque<Integer> stack = new ArrayDeque<>();
        int result = 0;
        int num = 0;
        int sign = 1;

        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (Character.isDigit(ch)) {
                num = num * 10 + ch - '0';
            } else if (ch == '+' || ch == '-') {
                result += sign * num;
                num = 0;
                sign = ch == '+' ? 1 : -1;
            } else if (ch == '(') {
                stack.push(result);
                stack.push(sign);
                result = 0;
                sign = 1;
            } else if (ch == ')') {
                result += sign * num;
                num = 0;
                result *= stack.pop();
                result += stack.pop();
            }
        }

        return result + sign * num;
    }
}
func calculate(s string) int {
    stack := make([]int, 0)
    result := 0
    num := 0
    sign := 1

    for i := 0; i < len(s); i++ {
        ch := s[i]
        if ch >= '0' && ch <= '9' {
            num = num*10 + int(ch-'0')
        } else if ch == '+' || ch == '-' {
            result += sign * num
            num = 0
            if ch == '+' {
                sign = 1
            } else {
                sign = -1
            }
        } else if ch == '(' {
            stack = append(stack, result)
            stack = append(stack, sign)
            result = 0
            sign = 1
        } else if ch == ')' {
            result += sign * num
            num = 0
            result *= stack[len(stack)-1]
            stack = stack[:len(stack)-1]
            result += stack[len(stack)-1]
            stack = stack[:len(stack)-1]
        }
    }

    return result + sign*num
}

复杂度分析

  • 时间复杂度:$O(n)$。每个字符只扫描一次,每个栈元素只进出一次。
  • 空间复杂度:$O(n)$。最坏情况下括号嵌套深度与字符串长度同阶。

关键点总结

  • 只有加减时,括号可以抽象成“外层结果 + 外层符号 × 内层结果”。
  • 栈保存的是括号外上下文,不是每个数字;这比通用双栈表达式求值更贴合本题。
  • 运算符负责结算前一个数字,字符串结束时还要补一次结算。
  • 若题目加入乘除,需要额外处理运算优先级,当前单纯的正负号模型就不够了。

易错点总结

  • 遇到运算符时必须先结算前一个数字;扫描结束也要补结算,否则 1+2 会漏掉末尾的 2。
  • 遇到 ) 时要先结算括号内最后一个数字,再恢复外层;(1+2) 中的 2 否则不会进入内层结果。
  • 左括号前的符号必须和外层结果一起保存。-(2+3) 若丢失符号会被算成 5。
  • 压栈与弹栈顺序必须匹配:代码先压 result 再压 sign,所以出栈时先取 sign,再取 result
  • 多位数要十进制拼接,空格要忽略;不能把 12 当作两个独立数字。

相似题目

题目 难度 考察点
150. 逆波兰表达式求值 中等 后缀表达式无括号无优先级,纯操作数栈即可
227. 基本计算器 II 中等 无括号但有乘除,需对上一个操作数延迟结算
772. 基本计算器 III 困难 括号与乘除并存,符号上下文叠加优先级处理
LCR 036. 逆波兰表达式求值 中等 150 的镜像题,巩固后缀求值的入栈出栈模板
面试题 16.26. 计算器 中等 227 的变体,练习中缀四则运算的一遍扫描写法