导读:本期聚焦于吴凌云创作的《C++如何用栈实现中缀表达式转后缀表达式?详解调度场算法原理与代码》,敬请观看详情。为什么计算器程序要先把中缀表达式转成后缀表达式?这背后的核心技术就是调度场算法。本文从表达式求值的痛点出发,先讲清楚中缀、后缀两种表示法的区别,再逐步拆解调度场算法的完整流程:操作符优先级如何比较、括号如何处理、栈内元素何时出栈。文章给出完整的C++实现代码,基于标准库的stack容器完成转换函数,并附带多组测试用例和逐行注释。同时分析了常见坑点,比如左括号与右括号的处理差异、相同优先级操作符的 结合顺序问题,最后延伸到如何基于后缀表达式完成求值,适合正在学习数据结构与算法的C++初学者和准备面试的开发者阅读。

写一个能计算四则运算表达式的程序,是很多C++学习者在学完栈之后必然会碰到的练习题。直接对中缀表达式求值并不容易,因为要同时处理运算符优先级和括号嵌套这两件麻烦事。经典的解决方案分两步走:先把中缀表达式转换成后缀表达式(也叫逆波兰表达式),再对后缀表达式求值,而第一步转换用的正是著名的调度场算法,它的核心数据结构就是栈。

一、先弄懂:什么是中缀表达式和后缀表达式

我们平时书写的数学表达式就是中缀表达式,运算符放在两个操作数中间,例如3 + 4 * 2。这种写法符合人类习惯,但对计算机来说却很别扭:要想正确求值,必须知道*的优先级高于+,遇到括号还得先算括号内的内容,解析过程需要反复回溯。

后缀表达式则把运算符放在操作数后面,上面的式子写成3 4 2 * +。它的最大好处是完全不需要括号,也不需要优先级规则,求值时只需要从左到右扫描,遇到数字就进栈,遇到运算符就弹出栈顶两个元素计算并把结果压回栈中,扫描结束后栈里剩下的就是答案。整个求值过程只依赖栈的先进后出特性,逻辑非常清晰。

再举一个带括号的例子:(1 + 2) * (3 - 4)对应的后缀表达式是1 2 + 3 4 - *。可以观察到,后缀表达式中运算符出现的顺序正好就是实际计算的顺序,这也是它能被线性扫描求值的根本原因。

二、调度场算法的完整流程

调度场算法由计算机科学家Dijkstra提出,名字来源于它的工作方式很像铁路调度场里编组车厢:操作数像车厢一样直接输出,运算符则先停在栈这条侧线上等待,直到时机合适才被调度出去。算法从左到右逐个读取中缀表达式的字符,规则如下:

  • 遇到数字(操作数),直接追加到输出结果中。
  • 遇到运算符,先比较它与栈顶运算符的优先级:如果栈顶运算符优先级更高,或优先级相同且当前运算符是左结合的,就把栈顶运算符依次弹出并输出,然后再把当前运算符压栈。
  • 遇到左括号,直接压入栈中,它相当于一道屏障。
  • 遇到右括号,不断弹出并输出栈顶运算符,直到遇到左括号,把左括号丢弃(左括号和右括号本身都不出现在后缀表达式里)。
  • 扫描结束后,把栈中剩余的运算符依次全部弹出输出。

这里有两个容易出错的细节需要特别强调。第一,左括号在栈内时的优先级应当被视为最低,这样后续的运算符才不会错误地把左括号弹出去。第二,对于相同优先级的运算符,加减乘除都是左结合的,也就是说a - b + c必须先算a - b,所以遇到同级运算符时栈顶要先弹出;但幂运算^是右结合的,2 ^ 3 ^ 2应该先算右边的3 ^ 2,处理方式正好相反。

3 + 4 * 2 / (1 - 5)为例手动推演一遍:读入3直接输出;读入+时栈为空,压栈;读入4输出;读入*,栈顶+优先级低,压栈;读入2输出;读入/,栈顶*同级且左结合,弹出*输出,再压入/;读入(压栈;读入1输出;读入-压栈;读入5输出;读入)弹出-输出,丢弃左括号;扫描结束,弹出/+。最终得到后缀表达式3 4 2 * 1 5 - / +

三、C++代码完整实现

下面给出一个基于std::stackstd::string的完整实现。为了简化处理,代码假设表达式中的操作数都是一位数字(0到9),运算符只包含加减乘除和小括号,且表达式本身合法。实际项目中如果要多位数字,只需在读取数字时连续读取即可。

#include <iostream>
#include <stack>
#include <string>
#include <cctype>

// 返回运算符优先级,数字越大优先级越高
int precedence(char op) {
    if (op == '+' || op == '-') return 1;
    if (op == '*' || op == '/') return 2;
    return 0; // 左括号等返回最低优先级
}

// 判断是否为运算符
bool isOperator(char ch) {
    return ch == '+' || ch == '-' || '*' || ch == '/';
}

// 核心函数:中缀表达式转后缀表达式
std::string infixToPostfix(const std::string& infix) {
    std::stack<char> opStack;   // 运算符栈
    std::string postfix;        // 存放后缀结果

    for (char ch : infix) {
        if (std::isspace(ch)) {
            continue; // 跳过空格
        }
        // 数字直接进入结果
        if (std::isdigit(ch)) {
            postfix += ch;
        }
        // 左括号直接压栈
        else if (ch == '(') {
            opStack.push(ch);
        }
        // 右括号:弹出直到遇到左括号
        else if (ch == ')') {
            while (!opStack.empty() && opStack.top() != '(') {
                postfix += opStack.top();
                opStack.pop();
            }
            opStack.pop(); // 丢弃左括号
        }
        // 运算符:弹出栈中优先级不低于当前的运算符
        else if (isOperator(ch)) {
            while (!opStack.empty() && opStack.top() != '('
                   && precedence(opStack.top()) >= precedence(ch)) {
                postfix += opStack.top();
                opStack.pop();
            }
            opStack.push(ch);
        }
    }
    // 把栈中剩余运算符全部输出
    while (!opStack.empty()) {
        postfix += opStack.top();
        opStack.pop();
    }
    return postfix;
}

int main() {
    std::string exprs[] = {
        "3+4*2/(1-5)",
        "(1+2)*(3-4)",
        "a-b+c" // 操作数也可为字母,思路相同
    };
    for (const auto& e : exprs) {
        std::cout << e << " -> " << infixToPostfix(e) << std::endl;
    }
    return 0;
}</code>

运行上面的程序,输出结果为342*15-/+12+34-*a-b+c,与我们前面手动推演的结果完全一致。代码中关键的一处是precedence(opStack.top()) >= precedence(ch)这个判断条件,其中的等号保证了同级运算符按左结合顺序处理。如果去掉等号,a-b+c就会错误地转换成abc+-,对应a-(b+c)的语义,结果就错了。

另一个值得注意的地方是主循环里对栈顶是否为左括号的显式判断。虽然precedence函数对左括号返回0,理论上已经能挡住比较,但显式写出opStack.top() != '('可以让逻辑更清晰,也能避免以后扩展优先级定义时引入隐藏bug。

四、从后缀表达式到最终求值

转成后缀表达式只是第一步,完整的计算器还需要对它求值。求值同样借助一个栈,规则是:从左到右扫描,遇到数字压栈;遇到运算符就从栈中弹出两个数字,注意先弹出的是右操作数,后弹出的是左操作数,计算左 op 右再把结果压回栈中。这一步的顺序弄反的话,减法和除法会直接算错。

int evaluatePostfix(const std::string& postfix) {
    std::stack<int> st;
    for (char ch : postfix) {
        if (std::isdigit(ch)) {
            st.push(ch - '0'); // 字符转数字
        } else {
            int right = st.top(); st.pop(); // 先弹出的是右操作数
            int left  = st.top(); st.pop(); // 后弹出的是左操作数
            switch (ch) {
                case '+': st.push(left + right); break;
                case '-': st.push(left - right); break;
                case '*': st.push(left * right); break;
                case '/': st.push(left / right); break;
            }
        }
    }
    return st.top();
}

infixToPostfixevaluatePostfix串联起来,就得到了一个最小可用的表达式计算器。如果要支持多位数和小数,常见做法是先对中缀字符串做词法分析,把每个完整数字切分成独立的token再放入队列,转换和求值逻辑本身几乎不用改动。

此外,还有一种进阶思路是把两次栈操作合并,在调度场算法中用两个栈同时处理转换和求值,扫描一遍表达式就能算出结果,避免生成中间的后缀字符串。不过分两步走的写法结构更清晰,也方便调试和扩展,比如把^幂运算加进来时只需调整优先级表并处理右结合特性即可,是初学者更值得掌握的版本。

中缀表达式转后缀表达式C++栈调度场算法修改时间:2026-08-31 07:32:55

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。