《大话数据结构》第4章实战:中缀表达式转后缀 + 后缀表达式求值(完整可运行实现)

1. 引言

四则运算表达式是我们从小学就开始接触的数学工具,比如 3+(4*5-2)/2。对人来说,一眼就能看出先算括号里的 4*5,再算 20-2,然后除以 2,最后加上 3。但计算机面对同样的表达式却一头雾水------它不知道运算符的优先级,也不理解括号的嵌套关系。

《大话数据结构》第 4.9 节用栈这一数据结构优雅地解决了四则运算问题。核心思路分两步:先把中缀表达式转成后缀表达式(逆波兰表示法),再对后缀表达式求值。这篇博客给出完整、可处理多位数和括号的 C++ 实现,并对算法细节做了深入剖析。

2. 核心概念:三种表达式

在正式写代码之前,先理清三种表达式的区别:

表达式类型 示例 特点
中缀表达式 3 + 4 * 5 运算符在两个操作数中间,符合人类习惯,但有优先级和括号问题
前缀表达式(波兰式) + 3 * 4 5 运算符在操作数前面,无需括号即可表达优先级
后缀表达式(逆波兰式) 3 4 5 * + 运算符在操作数后面,计算机求值最方便,无需括号

后缀表达式之所以适合计算机处理,是因为它天然消除了优先级和括号:遇到数字就压栈,遇到运算符就弹出两个操作数计算,结果再压回去。整个过程不需要前瞻或回溯。我们要做的,就是把中缀表达式转换成这种结构。

3. 中缀转后缀算法详解

3.1 算法核心思想

中缀转后缀的核心是用栈管理运算符。遍历中缀表达式的每个字符,按以下规则处理:

  • 遇到数字:直接输出到后缀表达式(注意处理多位数)。
  • 遇到左括号 (:直接压入运算符栈。
  • 遇到右括号 ):不断弹出栈顶运算符并输出,直到遇到左括号为止,最后把左括号弹出丢弃。
  • 遇到运算符 :和栈顶运算符比较优先级。如果栈顶运算符优先级大于等于当前运算符,就弹出栈顶并输出,直到栈为空或栈顶优先级更低,然后把当前运算符压栈。
  • 遍历结束后:把栈中剩余的运算符全部弹出输出。

下面用一个实例走一遍:中缀表达式 3+(4*5-2)/2 的转换过程。

步骤 当前字符 运算符栈 后缀输出 说明
1 3 3 数字直接输出
2 + + 3 栈空,+ 直接压栈
3 ( + ( 3 左括号直接压栈
4 4 + ( 3 4 数字直接输出
5 * + ( * 3 4 栈顶是 (* 直接压栈
6 5 + ( * 3 4 5 数字直接输出
7 - + ( - 3 4 5 * - 优先级低于 *,弹出 * 后压入 -
8 2 + ( - 3 4 5 * 2 数字直接输出
9 ) + 3 4 5 * 2 - 弹出 -,再弹出 ( 丢弃
10 / + / 3 4 5 * 2 - / 优先级高于 +,直接压栈
11 2 + / 3 4 5 * 2 - 2 数字直接输出
12 结束 3 4 5 * 2 - 2 / + 弹出剩余运算符 /+

最终后缀表达式为 3 4 5 * 2 - 2 / +,这正是我们期望的结果。

3.2 优先级函数

优先级判断是整个算法的基石。乘除的优先级高于加减,括号本身不参与优先级比较:

cpp 复制代码
int priority(char op) {
    if (op == '+' || op == '-') return 1;
    if (op == '*' || op == '/') return 2;
    return 0;  // 括号返回 0,保证不会错误弹出
}

这里把 ( 的优先级设为 0 是关键设计:当栈顶是左括号时,任何运算符都不会因为优先级比较而被错误弹出,因为左括号的优先级最低。

3.3 完整转换代码

cpp 复制代码
string infixToPostfix(const string& infix) {
    stack<char> opStack;
    string postfix;

    for (size_t i = 0; i < infix.size(); ++i) {
        char c = infix[i];

        if (isdigit(c)) {
            // 处理多位数:连续读取直到非数字
            while (i < infix.size() && isdigit(infix[i])) {
                postfix += infix[i++];
            }
            postfix += ' ';  // 用空格分隔操作数
            --i;             // 回退,外层 for 会 ++i
        }
        else if (c == '(') {
            opStack.push(c);
        }
        else if (c == ')') {
            // 弹出直到遇到左括号
            while (!opStack.empty() && opStack.top() != '(') {
                postfix += opStack.top();
                postfix += ' ';
                opStack.pop();
            }
            opStack.pop();  // 弹出 '(' 并丢弃
        }
        else if (c == '+' || c == '-' || c == '*' || c == '/') {
            // 栈顶优先级大于等于当前运算符时弹出
            while (!opStack.empty() && priority(opStack.top()) >= priority(c)) {
                postfix += opStack.top();
                postfix += ' ';
                opStack.pop();
            }
            opStack.push(c);
        }
    }

    // 弹出栈中剩余运算符
    while (!opStack.empty()) {
        postfix += opStack.top();
        postfix += ' ';
        opStack.pop();
    }

    return postfix;
}

这段代码有两个容易忽略的细节:

  • 多位数处理 :内层 while 循环连续读取数字字符,保证像 12345 这样的多位数被完整输出为一个操作数。
  • 空格分隔 :每个操作数和运算符后面都追加一个空格,这是为了让后缀表达式求值阶段能够用 stringstream 按空格切分 token。

4. 后缀表达式求值

4.1 算法思想

拿到后缀表达式后,求值反而简单了------只需要一个操作数栈。遍历后缀表达式中的每个 token:

  • 如果是数字,压入操作数栈。
  • 如果是运算符 ,从栈中弹出两个操作数(先弹出的是右操作数 b,后弹出的是左操作数 a),计算 a op b,把结果压回栈中。

遍历结束后,栈中剩下的唯一元素就是最终结果。

3 4 5 * 2 - 2 / + 为例:

步骤 当前 token 操作数栈 操作
1 3 [3] 数字入栈
2 4 [3, 4] 数字入栈
3 5 [3, 4, 5] 数字入栈
4 * [3, 20] 弹出 5 和 4,计算 4*5=20,压回
5 2 [3, 20, 2] 数字入栈
6 - [3, 18] 弹出 2 和 20,计算 20-2=18,压回
7 2 [3, 18, 2] 数字入栈
8 / [3, 9] 弹出 2 和 18,计算 18/2=9,压回
9 + [12] 弹出 9 和 3,计算 3+9=12,压回

最终栈中只剩 12,与预期一致。

4.2 完整求值代码

cpp 复制代码
double evaluatePostfix(const string& postfix) {
    stack<double> numStack;
    stringstream ss(postfix);
    string token;

    while (ss >> token) {
        // 判断是否为数字(支持负数如 "-3")
        if (isdigit(token[0]) || (token.size() > 1 && token[0] == '-')) {
            numStack.push(stod(token));
        } else {
            double b = numStack.top(); numStack.pop();
            double a = numStack.top(); numStack.pop();
            switch (token[0]) {
                case '+': numStack.push(a + b); break;
                case '-': numStack.push(a - b); break;
                case '*': numStack.push(a * b); break;
                case '/': numStack.push(a / b); break;
            }
        }
    }

    return numStack.top();
}

注意这里用 stod(token) 将字符串转为 double,这样即使表达式中有小数也能正确处理。另外数字判断中加了 token.size() > 1 && token[0] == '-',是为了识别像 -3 这样的负数 token。

5. 完整可运行代码

把上述两部分拼在一起,加上必要的头文件和测试用例,得到一份完整的可运行程序:

cpp 复制代码
#include <iostream>
#include <stack>
#include <string>
#include <sstream>
#include <cctype>
using namespace std;

int priority(char op) {
    if (op == '+' || op == '-') return 1;
    if (op == '*' || op == '/') return 2;
    return 0;
}

string infixToPostfix(const string& infix) {
    stack<char> opStack;
    string postfix;

    for (size_t i = 0; i < infix.size(); ++i) {
        char c = infix[i];
        if (isdigit(c)) {
            while (i < infix.size() && isdigit(infix[i])) {
                postfix += infix[i++];
            }
            postfix += ' ';
            --i;
        } else if (c == '(') {
            opStack.push(c);
        } else if (c == ')') {
            while (!opStack.empty() && opStack.top() != '(') {
                postfix += opStack.top();
                postfix += ' ';
                opStack.pop();
            }
            opStack.pop();
        } else if (c == '+' || c == '-' || c == '*' || c == '/') {
            while (!opStack.empty() && priority(opStack.top()) >= priority(c)) {
                postfix += opStack.top();
                postfix += ' ';
                opStack.pop();
            }
            opStack.push(c);
        }
    }

    while (!opStack.empty()) {
        postfix += opStack.top();
        postfix += ' ';
        opStack.pop();
    }

    return postfix;
}

double evaluatePostfix(const string& postfix) {
    stack<double> numStack;
    stringstream ss(postfix);
    string token;

    while (ss >> token) {
        if (isdigit(token[0]) || (token.size() > 1 && token[0] == '-')) {
            numStack.push(stod(token));
        } else {
            double b = numStack.top(); numStack.pop();
            double a = numStack.top(); numStack.pop();
            switch (token[0]) {
                case '+': numStack.push(a + b); break;
                case '-': numStack.push(a - b); break;
                case '*': numStack.push(a * b); break;
                case '/': numStack.push(a / b); break;
            }
        }
    }

    return numStack.top();
}

int main() {
    // 测试用例 1
    string infix1 = "3+(4*5-2)/2";
    string postfix1 = infixToPostfix(infix1);
    cout << "中缀: " << infix1 << endl;
    cout << "后缀: " << postfix1 << endl;
    cout << "结果: " << evaluatePostfix(postfix1) << endl;  // 12

    cout << "---" << endl;

    // 测试用例 2:多位数
    string infix2 = "12+34*5";
    string postfix2 = infixToPostfix(infix2);
    cout << "中缀: " << infix2 << endl;
    cout << "后缀: " << postfix2 << endl;
    cout << "结果: " << evaluatePostfix(postfix2) << endl;  // 182

    cout << "---" << endl;

    // 测试用例 3:嵌套括号
    string infix3 = "((2+3)*4-(5-2))/3";
    string postfix3 = infixToPostfix(infix3);
    cout << "中缀: " << infix3 << endl;
    cout << "后缀: " << postfix3 << endl;
    cout << "结果: " << evaluatePostfix(postfix3) << endl;  // 5.66667

    return 0;
}

6. 运行结果与分析

编译运行上述代码,输出如下:

text 复制代码
中缀: 3+(4*5-2)/2
后缀: 3 4 5 * 2 - 2 / + 
结果: 12
---
中缀: 12+34*5
后缀: 12 34 5 * + 
结果: 182
---
中缀: ((2+3)*4-(5-2))/3
后缀: 2 3 + 4 * 5 2 - - 3 / 
结果: 5.66667

三个测试用例覆盖了基础四则运算与括号嵌套多位数处理多层嵌套括号三种典型场景,全部输出正确结果。

7. 拓展思考

这套代码已经能正确处理多位数、括号和四则运算优先级。如果想让实现更健壮,以下几个方向值得进一步探索:

  • 支持小数 :当前代码只处理整数数字,可以在 isdigit 判断中加入对小数点 . 的处理。
  • 支持负数 :中缀表达式中的一元负号(如 -3+53*(-2))需要特殊处理,因为同一个 - 符号可能是二元减法也可能是一元取负。
  • 支持更多运算符 :比如幂运算 ^(右结合)或取模 %,需要调整优先级函数和结合性规则。
  • 错误处理:对非法输入(如不匹配的括号、连续运算符等)给出友好提示。
  • 用模板/泛型改写 :让求值函数支持 intlong longdouble 等多种数值类型。

8. 总结

中缀转后缀加后缀求值,是栈的经典应用场景,也是《大话数据结构》第 4 章的高光内容。整个方案的核心就两句话:

  • 转后缀靠运算符栈------用优先级比较决定运算符的输出时机,括号用来界定子表达式的边界。
  • 求值靠操作数栈------见到数字就压,见到运算符就弹两个算一个再压回去。

理解了这个双栈协作的模式,再去理解编译原理中的表达式解析、甚至是逆波兰计算器的实现,都会轻松很多。建议读者把代码复制到本地跑一遍,然后试着改几个表达式观察后缀输出,手感会更好。

相关推荐
phoenix@Capricornus1 小时前
从统计决策到贝叶斯估计
人工智能·算法·机器学习
AICDragon1 小时前
1.8%就够了:K3的896个MoE专家,为什么激活率这么低反而是好事?
人工智能·算法
VALENIAN瓦伦尼安教学设备2 小时前
ASHOOTER激光对中仪如何通过颜色确定调整是否合适
数据库·嵌入式硬件·算法
货拉拉技术2 小时前
重塑 Agent 度量衡:基于 LLM-as-a-Judge 的离线评估体系与实践
算法·设计模式
车压2 小时前
从“为什么”理解注意力机制
算法
wabs6662 小时前
关于哈希表【力扣15.三数之和的思考】
数据结构·算法·leetcode·散列表·哈希表·三数之和
退休倒计时2 小时前
【每日一题】LeetCode 88. 合并两个有序数组 TypeScript
算法·leetcode·职场和发展·typescript
键盘会跳舞2 小时前
C++:函数对象与 std::function 源码级深度拆解——泛型算法的策略内核与可调用对象统一封装
c++·算法·仿函数
手写码匠2 小时前
华为云Flexus+DeepSeek征文|Agent 评测体系实战:用 DeepSeek-R1 当裁判,打造 Dify Agent 的自动化回归测试流水线
人工智能·深度学习·算法·aigc