写一个能计算四则运算表达式的程序,是很多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::stack和std::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();
}把infixToPostfix和evaluatePostfix串联起来,就得到了一个最小可用的表达式计算器。如果要支持多位数和小数,常见做法是先对中缀字符串做词法分析,把每个完整数字切分成独立的token再放入队列,转换和求值逻辑本身几乎不用改动。
此外,还有一种进阶思路是把两次栈操作合并,在调度场算法中用两个栈同时处理转换和求值,扫描一遍表达式就能算出结果,避免生成中间的后缀字符串。不过分两步走的写法结构更清晰,也方便调试和扩展,比如把^幂运算加进来时只需调整优先级表并处理右结合特性即可,是初学者更值得掌握的版本。
中缀表达式转后缀表达式C++栈调度场算法修改时间:2026-08-31 07:32:55