青训营X豆包MarsCode 技术训练营第八篇——简单四则运算解析器(题解与总结)| 豆包MarsCode AI 刷题

52 阅读5分钟

方向一——简单四则运算解析器(栈)

这次我们来讲一讲怎么使用栈这个数据结构解决这道题,并且由此找到学习各种数据结构的方法。

题目背景:

我们需要实现一个基本的计算器来计算一个简单的字符串表达式的值。这个表达式包含数字、运算符 +-*/ 和括号 ()。要求不使用任何内置的 eval() 函数,并且要计算出整数结果(除法保留整数部分)。这个题目主要考察的是如何实现表达式求值,特别是如何处理运算符优先级和括号。

解题思路:

解决这个问题通常有几种方式,其中一种比较经典的方法是使用 数据结构。我们可以通过栈来模拟表达式的计算过程,栈能够帮助我们按照运算符优先级来处理操作符,同时在遇到括号时能够正确地处理子表达式。

关键步骤:

  1. 运算符优先级:

    • 运算符的优先级关系:* 和 / 的优先级高于 + 和 -
    • 在栈中,可以用一个映射来记录不同运算符的优先级。
  2. 括号处理:

    • 遇到左括号 ( 时,表示遇到一个新的子表达式,应该将当前的运算环境(栈)保存下来,直到右括号 ) 出现。
    • 在遇到右括号 ) 时,应该执行栈中积累的操作,直到遇到对应的左括号。
  3. 栈的使用:

    • 使用两个栈:一个栈 num 来存储操作数,一个栈 op 来存储操作符。
    • 遇到数字时,直接入栈;遇到运算符时,根据栈中当前运算符的优先级,决定是否执行之前的操作。
    • 最后,当整个表达式遍历完后,仍然会有剩余的操作符在栈中,这时需要依次处理。

算法步骤:

  1. 遍历表达式:从左到右逐个字符处理。
  2. 遇到数字:解析连续的数字,入栈。
  3. 遇到运算符:根据运算符优先级,决定是否执行栈中的运算,执行后将当前运算符入栈。
  4. 遇到左括号 ( :将其入栈,表示进入一个子表达式。
  5. 遇到右括号 ) :执行直到遇到左括号的操作,计算完后弹出左括号。
  6. 处理完所有字符后:栈中可能还有操作符,逐个计算直到栈为空。

代码实现:

cppCopy Code
#include <bits/stdc++.h>
using namespace std;

int solution(string expression) {
    stack<int> num;  // 存储数字
    stack<char> op;  // 存储运算符
    map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};  // 运算符优先级映射

    // 用来执行运算的lambda表达式
    auto eval = [&] {
        int a = num.top(); num.pop();  // 弹出两个数字
        int b = num.top(); num.pop();
        char c = op.top(); op.pop();  // 弹出运算符
        int x;
        if (c == '+') x = a + b;
        else if (c == '-') x = b - a;
        else if (c == '*') x = a * b;
        else x = b / a;  // 注意整数除法
        num.push(x);  // 将结果压入数字栈
    };

    for (int i = 0; i < expression.size(); i++) {
        char t = expression[i];

        if (isdigit(t)) {
            // 如果是数字,处理可能多位数的情况
            int numValue = 0;
            while (i < expression.size() && isdigit(expression[i])) {
                numValue = numValue * 10 + (expression[i] - '0');
                i++;
            }
            i--;  // 回退一位,因为i在循环中会被多加一次
            num.push(numValue);
        }
        else if (t == '(') {
            op.push(t);  // 遇到左括号,入栈
        }
        else if (t == ')') {
            // 遇到右括号,弹出运算符直到遇到左括号
            while (op.top() != '(') eval();
            op.pop();  // 弹出左括号
        }
        else {
            // 遇到运算符,先计算栈中已有的优先级更高或相等的运算符
            while (op.size() && op.top() != '(' && pr[op.top()] >= pr[t]) {
                eval();
            }
            op.push(t);  // 当前运算符入栈
        }
    }

    // 遍历结束后,栈中可能还剩下运算符,依次计算
    while (!op.empty()) {
        eval();
    }

    // 最终栈中应该只剩一个数字,返回结果
    return num.top();
}

int main() {
    // 测试用例
    cout << (solution("1+1") == 2) << endl;  // 2
    cout << (solution("3+4*5/(3+2)") == 7) << endl;  // 7
    cout << (solution("4+2*5-2/1") == 12) << endl;  // 12
    cout << (solution("(1+(4+5+2)-3)+(6+8)") == 23) << endl;  // 23
    cout << (solution("2*(5+5*2)/3+(6+8*3)") == 40) << endl;  // 40
    return 0;
}

代码解析:

  1. 栈的使用

    • 使用 num 栈存储数字,op 栈存储运算符。
    • 运算符优先级通过 map 来存储,便于查找和比较。
  2. 数字处理

    • 在遍历表达式时,如果遇到数字(isdigit(t)),则处理连续数字,构造出完整的数字并入栈。
  3. 运算符处理

    • 当遇到运算符时,首先检查栈顶运算符的优先级。如果栈顶运算符优先级更高或相同,就先进行计算,再将当前运算符压入栈。
  4. 括号处理

    • 左括号直接入栈,表示进入子表达式。
    • 右括号遇到时,执行栈中的运算,直到左括号为止,表示结束子表达式。
  5. 最终计算

    • 完成整个表达式遍历后,栈中可能仍然有操作符,继续依次计算直到栈为空,最后返回栈顶的结果。

测试样例:

  • "1+1" -> 2
  • "3+4*5/(3+2)" -> 7
  • "4+2*5-2/1" -> 12
  • "(1+(4+5+2)-3)+(6+8)" -> 23
  • "2*(5+5*2)/3+(6+8*3)" -> 40

总结:

这道题考察了我们如何使用栈来处理数学表达式的运算,特别是如何处理运算符的优先级和括号的嵌套。通过模拟计算过程,可以有效地解决问题。

建议:

建议初学者可以一次弄懂一个数据结构,最好把这种数据结构的典型例题及变式做了,比如队列,可以做约瑟夫环;栈,可以做括号匹配,四则运算,还有任意进制转换;图可以做最短路径等等。