四则运算表达式求值

100 阅读6分钟

四则运算表达式求值

问题背景

我们需要实现一个计算器,它能够解析一个以字符串形式表示的、仅包含整数和基本四则运算符的数学表达式,并计算出其结果。

计算规则

  1. 运算符: 表达式中只包含加 +、减 -、乘 *、除 / 四种运算符。

  2. 运算优先级:

    • */ 的计算优先级高于 +-
    • 相同优先级的运算符按照从左到右的顺序计算。
  3. 整数运算: 所有运算均为整数运算。对于除法,结果需要向零取整(例如,5/2=2, -5/2=-2)。

  4. 错误处理: 在计算过程中,如果出现除以零的情况,应立即停止计算,并将结果标记为错误。

任务要求

给定一个字符串形式的计算表达式 expression,请根据上述规则对其求值。

  • 如果计算成功,以字符串形式返回最终的计算结果。
  • 如果计算过程中遇到除以零的情况,则返回字符串 "error"

输入格式

  • expression: 一个字符串,代表待计算的数学表达式。

    • 1 <= expression.length <= 100

输出格式

  • 一个表示十进制整数的字符串;或者,在发生除零错误时,输出固定的错误字符串 "error"

限制与要求

  • 时间限制: C/C++ 1000ms, 其他语言 2000ms

  • 内存限制: C/C++ 64MB, 其他语言 128MB

  • 用例保证:

    • 输入的数字、计算过程中的中间值以及最终结果,都在32位有符号整数(int)的范围内。
    • 输入表达式的格式合法。

样例说明

样例 1

  • 输入: "1+2*3-100/2"

  • 输出: "-43"

  • 解释:

    1. 根据运算符优先级,首先计算乘法和除法。

      • 2 * 3 = 6
      • 100 / 2 = 50
    2. 原表达式等价于计算 "1 + 6 - 50"

    3. 从左到右依次计算加法和减法。

      • 1 + 6 = 7
      • 7 - 50 = -43
    4. 最终结果转换为字符串为 "-43"

样例 2

  • 输入: "3/0"

  • 输出: "error"

  • 解释:

    1. 表达式中包含 3 / 0 的计算。
    2. 在执行除法时,检测到除数为零。
    3. 计算立即停止,并按要求返回错误字符串 "error"
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Scanner;

/**
 * LeetCode 227 / 类似问题: 基本计算器 II
 * 解决包含 +、-、*、/ 四则运算的字符串表达式求值问题。
 *
 * 核心思想:双栈法
 * 使用一个栈存储操作数(数字),另一个栈存储操作符。
 * 遍历表达式字符串,根据操作符的优先级来决定是立即计算还是先将操作符入栈。
 *
 * 算法步骤:
 * 1. 初始化一个操作数栈 (numStack) 和一个操作符栈 (opStack)。
 * 2. 从左到右遍历表达式字符串中的每个字符。
 * a. 如果是数字,则解析出完整的整数,并压入 numStack。
 * b. 如果是操作符,则与 opStack 栈顶的操作符进行优先级比较:
 * - 只要 opStack 不为空,且栈顶操作符的优先级 **大于或等于** 当前操作符的优先级,
 * 就从 opStack 弹出一个操作符,从 numStack 弹出两个操作数进行计算,然后将结果压回 numStack。
 * - 循环比较结束后,将当前操作符压入 opStack。
 * 3. 遍历完整个字符串后,opStack 中可能还有剩余的操作符。
 * 依次弹出操作符和操作数进行计算,直到 opStack 为空。
 * 4. 最终,numStack 中剩下的唯一一个数字就是整个表达式的结果。
 * 5. 在计算过程中,特别处理除法操作,如果除数为 0,则立即停止并返回 "error"。
 */
public class Solution {
    /**
     * 对给定的字符串形式的四则运算表达式进行求值。
     *
     * @param s 包含数字和 '+', '-', '*', '/' 的字符串表达式。
     * @return 表达式的计算结果,以字符串形式返回。如果计算过程中出现除零,则返回 "error"。
     */
    public String calculate(String s) {
        // Deque (双端队列) 是 Java 中推荐用来实现栈的数据结构
        // numStack: 存储操作数
        Deque<Integer> numStack = new ArrayDeque<>();
        // opStack: 存储操作符
        Deque<Character> opStack = new ArrayDeque<>();

        try {
            // 使用 for 循环遍历整个表达式字符串
            for (int i = 0; i < s.length(); i++) {
                char c = s.charAt(i);

                // 跳过空格
                if (c == ' ') {
                    continue;
                }

                // 如果当前字符是数字
                if (Character.isDigit(c)) {
                    // 解析一个完整的数字(可能不止一位)
                    int num = 0;
                    while (i < s.length() && Character.isDigit(s.charAt(i))) {
                        // (s.charAt(i) - '0') 将字符数字转换为整数值
                        num = num * 10 + (s.charAt(i) - '0');
                        i++;
                    }
                    // 将解析出的数字压入操作数栈
                    numStack.push(num);
                    // 因为 for 循环的 i++ 会额外执行一次,而 while 循环已经将 i 移动到数字末尾的下一个位置,
                    // 所以需要 i-- 来抵消 for 循环多余的自增,确保下一次循环从正确的位置开始。
                    i--;
                }
                // 如果当前字符是操作符
                else {
                    // 规则:只要操作符栈不为空,且栈顶操作符的优先级 >= 当前操作符的优先级
                    // 就要先计算栈顶的操作(保证了从左到右的计算顺序和乘除优先)
                    while (!opStack.isEmpty() && precedence(opStack.peek()) >= precedence(c)) {
                        calculateTop(numStack, opStack);
                    }
                    // 将当前操作符压入操作符栈
                    opStack.push(c);
                }
            }

            // --- 遍历完整个表达式字符串后 ---
            // 将栈中剩余的所有操作符依次计算
            while (!opStack.isEmpty()) {
                calculateTop(numStack, opStack);
            }

            // 最终,数字栈中剩下的唯一一个数就是结果
            // 处理输入为空或格式错误导致栈为空的情况
            if (numStack.isEmpty()) {
                return "0"; // 或者根据题意返回错误
            }
            return String.valueOf(numStack.pop());

        } catch (ArithmeticException e) {
            // 如果在计算过程中捕获到我们主动抛出的除零异常,返回 "error"
            return "error";
        }
    }

    /**
     * 从栈顶取出一个操作符和两个操作数进行计算,并将结果压回操作数栈。
     *
     * @param numStack 操作数栈
     * @param opStack  操作符栈
     * @throws ArithmeticException 如果发生除零操作
     */
    private void calculateTop(Deque<Integer> numStack, Deque<Character> opStack) {
        // 检查操作数是否足够
        if (numStack.size() < 2) {
             throw new ArithmeticException("Invalid Expression");
        }
        
        // 弹出两个操作数和一个操作符
        // 注意顺序:栈是后进先出,所以先弹出的是右操作数
        int num2 = numStack.pop();
        int num1 = numStack.pop();
        char op = opStack.pop();

        // 执行计算
        int result;
        switch (op) {
            case '+':
                result = num1 + num2;
                break;
            case '-':
                result = num1 - num2;
                break;
            case '*':
                result = num1 * num2;
                break;
            case '/':
                // 检查除零错误
                if (num2 == 0) {
                    throw new ArithmeticException("Division by zero");
                }
                result = num1 / num2;
                break;
            default:
                // 不应发生,因为输入字符受限
                throw new IllegalArgumentException("Invalid operator: " + op);
        }
        // 将计算结果压回操作数栈
        numStack.push(result);
    }

    /**
     * 定义运算符的优先级。
     *
     * @param op 操作符
     * @return 优先级数值(数值越大,优先级越高)
     */
    private int precedence(char op) {
        if (op == '+' || op == '-') {
            return 1; // 加减优先级低
        }
        if (op == '*' || op == '/') {
            return 2; // 乘除优先级高
        }
        // 对于其他字符(例如括号,虽然本题没有),可以返回 0
        return 0;
    }
}


class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String expression = scanner.nextLine(); // 读取一行表达式
        scanner.close();

        // 创建 Solution 类的实例并调用方法
        Solution solution = new Solution();
        System.out.println(solution.calculate(expression));
    }
}