题解:简单四则运算解析器 | 豆包MarsCode AI刷题

210 阅读7分钟

题目解析

先附上青训营中的题目链接 52. 简单四则运算解析器

这道题是非常经典的一道算法题了,它的核心是实现一个支持加减乘除及括号运算的简单表达式解析器,考察的是栈的应用以及中缀表达式的计算规则。由于不能使用内置的 eval 函数,必须手动解析字符串,并实现操作符优先级处理、括号匹配及运算操作。

算法的核心思路是通过两个栈(一个存储操作数,另一个存储操作符)模拟表达式的解析与计算过程。这种方式适用于绝大多数简单的数学表达式解析任务。

解题思路

1. 表达式的解析规则

表达式的计算顺序受到操作符优先级和括号嵌套的限制,因此我们需要遵循以下规则:

优先级处理:乘法和除法优先于加法和减法执行;

括号嵌套:括号内的表达式优先计算;

顺序计算:在优先级和括号影响下,从左至右逐步解析并计算。

解析时,我们需要维护一个运算环境,使得每个操作符都能按优先级正确处理。

2. 数据结构设计

为了实现上述解析规则,使用两个栈:

  1. 数字栈(numberStack) :用于存储操作数;

  2. 操作符栈(operatorStack) :用于存储操作符和括号。

栈的特点是后进先出(LIFO),非常适合处理括号嵌套和优先级问题。当遇到右括号或者优先级较低的操作符时,可以通过栈顶操作实现当前解析范围的子表达式计算。

3. 运算符优先级

为了正确处理操作符,我们通过一个哈希表定义优先级:

• + 和 - 的优先级为 0;

• * 和 / 的优先级为 1。

优先级的作用是在栈顶存在高优先级操作符时,要求先进行栈顶操作计算,确保计算顺序正确。

4. 解析过程

解析过程分为以下几步:

  1. 逐字符扫描:逐一读取表达式的字符,根据其类型(数字、操作符、括号)采取不同操作。

  2. 处理数字:如果是数字,直接转换为整数并入 numberStack。

  3. 处理操作符:判断当前操作符与栈顶操作符优先级关系:

• 若栈顶操作符优先级高,则出栈进行运算;

• 否则直接将当前操作符入栈。

  1. 处理括号

• 遇到左括号时,直接入 operatorStack;

• 遇到右括号时,不断弹出操作符栈并计算,直到匹配到对应的左括号。

  1. 处理剩余操作符:扫描完成后,依次从栈中取出操作符和操作数完成计算。

5. 处理剩余操作符

在扫描完成后,栈中可能仍然存在未计算的操作符。需要将 operatorStack 逐步清空,依次从 numberStack 中取出两个数字进行运算。

代码实现

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

public class Main {

    private static int applyOperation(int a, int b, char op) {
        switch (op) {
            case '+':
                return a + b;
            case '-':
                return a - b;
            case '*':
                return a * b;
            case '/':
                return a / b;
            default:
                throw new IllegalArgumentException("Invalid operator: " + op);
        }
    }

    public static int solution(String expression) {
        // Please write your code here
        Stack<Integer> numberStack = new Stack<>();
        Stack<Character> operatorStack = new Stack<>();

        // 操作符优先级
        Map<Character, Integer> map = new HashMap<>() {
            {
                put('+', 0);
                put('-', 0);
                put('*', 1);
                put('/', 1);
            }
        };

        for (char ch : expression.toCharArray()) {
            if (ch >= '0' && ch <= '9')
                numberStack.push(ch - '0'); // 数字直接入栈
            else if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
                while (!operatorStack.isEmpty() && operatorStack.peek() != '(' && map.get(ch) <= map.get(operatorStack.peek()))
                {
                    // 操作栈不为空,栈顶不为左括号且优先级比外面低,就压入
                    int b = numberStack.pop();
                    int a = numberStack.pop();
                    char op = operatorStack.pop();
                    numberStack.push(applyOperation(a, b, op));
                }
                // 否则操作符入栈
                operatorStack.push(ch);
            } else if (ch == '(') {
                operatorStack.push(ch);
            } else if (ch == ')')// 遇到右括号,弹出栈顶操作符进行运算,直到遇到左括号并弹出
            {
                while (operatorStack.peek() != '(') {
                    char op = operatorStack.pop();
                    int b = numberStack.pop();
                    int a = numberStack.pop();
                    numberStack.push(applyOperation(a, b, op));
                }
                operatorStack.pop();
            }
        }
        // 扫描完毕,继续运算
        while (!operatorStack.empty()) {
            char op = operatorStack.pop();
            int b = numberStack.pop();
            int a = numberStack.pop();
            numberStack.push(applyOperation(a, b, op));
        }

        return numberStack.peek();
    }

    public static void main(String[] args) {
        // You can add more test cases here
        System.out.println(solution("1+1") == 2);
        System.out.println(solution("3+4*5/(3+2)") == 7);
        System.out.println(solution("4+2*5-2/1") == 12);
        System.out.println(solution("(1+(4+5+2)-3)+(6+8)") == 23);
    }
}

一些Tips & 个人思考

这道题目表面上看是实现一个简单的计算器,但其核心包含了许多计算逻辑与数据结构知识的结合,尤其是在表达式解析、栈的应用以及算法优化等方面。完成这道题后,我有以下几点心得与收获:

1. 栈的灵活性和重要性

栈是表达式解析中不可或缺的数据结构,它的后进先出的特点完美契合了括号匹配和操作符优先级的需求。在这道题中,我们通过两个栈分别管理操作数和操作符,解决了从中缀表达式直接计算结果的问题。

在解决过程中,我对栈的理解更进一步,尤其是通过动态调整栈的内容解决不同优先级的操作。栈的这种灵活性让我意识到,它不仅能用于简单的括号匹配,还可以很好地解决复杂问题。未来在涉及嵌套结构或递归问题时,栈无疑是一个重要工具。

2. 动态优先级处理的设计

这道题的关键之一是如何正确处理操作符的优先级。加减法和乘除法的优先级不同,需要在解析时动态调整计算顺序。通过设计优先级的哈希表,并结合操作符栈的内容判断是否需要出栈运算,我发现这种方法非常高效且易于扩展。

这一思路让我意识到,解决问题时,动态调整优先级不仅限于计算问题,还可以应用在其他需要排序或优先处理的任务中。例如,在操作系统的任务调度中,动态优先级也可以根据当前的状态进行调整,这种思想是一种通用的解决方案。

3. 对括号嵌套的处理

括号是表达式解析中最复杂的部分之一,它不仅改变了运算的顺序,还需要正确地匹配左右括号。在实现过程中,我采用了简单但有效的方案:左括号直接入栈,右括号则触发括号内子表达式的计算,直到匹配到对应的左括号为止。

这一设计让我明白了解决复杂问题时,分而治之是非常有效的策略。将嵌套问题拆解为独立的小问题,逐步解决,既能保证逻辑清晰,也能减少错误发生的可能性。

4. 边界条件的思考

在实现过程中,我还特别关注了一些边界情况,例如:

• 空字符串应该返回0;

• 操作符和数字的连续性是否合法;

• 除法时如何处理除以零的问题(题目假设输入有效,不需特殊处理)。

虽然这些边界条件在题目描述中没有明确要求,但在实际开发中,处理边界情况是写好健壮代码的关键。这次练习让我更加重视边界条件对程序正确性的影响。

5. 算法的可扩展性

当前实现的解析器只支持简单的整数加减乘除运算,但这一框架具有很强的可扩展性。例如:

支持多位数和小数:通过引入一个缓冲区(StringBuilder)来处理连续的数字字符,支持多位数解析。

支持更多操作符:通过扩展优先级表,增加如幂运算(^)、取模运算(%)等高级运算。

支持函数调用:通过将函数解析为嵌套表达式,扩展解析器的功能,如支持 sin(), cos(), log() 等。

支持变量和动态输入:结合符号表解析变量名,或通过动态赋值支持用户自定义表达式。

这道题让我感受到,解决问题不仅仅是完成当前任务,还要思考解决方案在更广泛场景中的适用性。一个设计良好的框架能够承载更多的功能扩展,而不需要从头再写。

6. 表达式解析的实际意义

表达式解析是编译器、脚本引擎和数据处理工具中的核心功能。例如,在编译器中,解析器需要将复杂的代码表达式转化为语法树;在数据库中,查询解析器需要将 SQL 表达式翻译为可执行的操作计划。通过这道题,我对表达式解析的基本实现有了直观的感受,也更理解实际开发中解析任务的重要性。