目录

题目描述

772. 基本计算器 III

题意分析

给一个字符串形式的算术表达式,求它的值。表达式里可能出现非负整数、四则运算符 + - * /、圆括号,以及任意位置的空格。

要什么:一个整数结果。除法是整数除法,向零截断(例如 3/2 得 1)。

约束信号有三条。第一,运算符有优先级:乘除高于加减。这意味着不能简单地从左到右一路算下去。第二,有括号:括号内的表达式必须先算完,而且括号可以任意嵌套。第三,表达式保证合法,不会出现除零、不会有多余的运算符,也不会出现一元负号——所有减号都是二元的。这一点很重要,它省掉了大量特判。

空格可以出现在任何位置,包括数字和运算符之间,所以读取时必须随时准备跳过它们,但空格本身不携带任何语义,不能影响当前正在攒的数字或已记录的运算符。

边界情况包括:整个表达式就是一个数字;括号紧跟在运算符后面;括号内又嵌括号;表达式末尾是数字(后面没有运算符来触发结算);以及多位数的连续读取。

解法:递归处理括号 + 两级累计

核心思路

括号与运算符优先级是两个独立问题:括号表示“先把这一段算成一个操作数”,适合递归;同一层内只有四则运算,用两个累计量即可处理优先级,不必构造表达式树,也不必额外维护栈。

解析每一层时维护:

  • sum:已经被加减号封口、以后不会再改变的项之和。
  • term:当前尚未封口的乘除链结果。
  • op:当前操作数前面的运算符。

读到操作数 num 后,若 op+-,先把旧 term 加入 sum,再以 num-num 开启新项;若是 */,直接更新 term。这样乘除立即结算,加减延迟结算,优先级自然成立。

遇到 ( 就递归解析,返回值被当成普通数字;当前层遇到 ) 时返回。所有递归共享扫描位置,因此每个字符只经过一次。循环不变量是:sum + term 始终等于当前层已读表达式的值。四种运算都按其结合规则更新这两个量,所以扫描结束返回 sum + term 正确。

解题步骤

  1. 从下标 0 调用解析函数,解析函数负责处理当前位置到同层右括号或字符串末尾。
  2. 跳过空格后读取一个操作数:连续数字组成多位数;若遇到左括号,则递归取得括号内结果。
  3. 根据前一个运算符更新 sumterm:加减封口旧项,乘除延长当前项。
  4. 读取下一个运算符;若当前层遇到右括号,消费右括号并把本层结果返回。
  5. 顶层扫描结束后返回 sum + term

2*(5+5*2)/3 为例:括号层把 5+5*2 算成 15;顶层的 term 依次变为 2、30、10,最终结果为 10。若把乘法也延迟到求和阶段,就会错误地把括号层算成 5+5+2,这正是必须单独维护 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
}

复杂度分析

  • 时间复杂度:$O(n)$。递归层共享且只向前推进同一个下标,每个字符只被读取常数次。
  • 空间复杂度:$O(h)$,其中 $h$ 是括号最大嵌套深度;除递归栈外只使用常数状态。

关键点总结

  • 括号递归返回一个数,同层再用 sum + term 处理两级优先级。
  • op 表示当前操作数前面的运算符,初始必须是 +
  • 不变量是 sum 存已封口项、term 存当前乘除项,因此 sum + term 始终等于已读部分。
  • 解析函数消费右括号并返回新下标,避免上层重复读取。
  • 面试追问若要求支持更复杂语法,再升级为明确的 expression / term / factor 递归下降;本题只有固定四则运算,不需要先建语法树。

易错点总结

  • 只在读到下一个运算符时结算,会漏掉表达式最后一个操作数。
  • 乘除直接并入 sum 会破坏优先级;例如 1+2*3 应为 7。
  • 递归层不共享或不返回扫描位置,会重复解析括号内容。
  • 返回前忘记消费 ),上层会把右括号误当成运算符。
  • 多位数必须按 num = num * 10 + digit 累积。
  • Java 和 Go 的整数除法都向零截断,不能改成向下取整。

相似题目

题目 难度 考察点
227. 基本计算器 II 中等 无括号的优先级处理
224. 基本计算器 困难 括号与一元负号
150. 逆波兰表达式求值 中等 后缀表达式栈求值
LCR 036. 逆波兰表达式求值 中等 后缀式的整数除法语义
面试题 16.26. 计算器 中等 单层四则运算模拟