题目解析
这道题目(洛谷 P1175)是练习**栈(Stack)**应用的经典题目。它考察了两个核心环节:
- 中缀表达式转后缀表达式(逆波兰表达式)。
- 后缀表达式的计算,并要求输出每一步的计算过程。
虽然标准的后缀表达式计算通常使用栈来完成,但由于题目要求输出每一步的中间形态(即未计算的后缀部分还要保留),直接使用纯栈来做计算部分的输出会比较麻烦(因为栈底元素看不见)。因此,这道题最好的解法是:用栈做转换,用动态数组(或列表)做计算演示。
下面我将分步解析解题思路和代码。
第一步:中缀转后缀(利用栈)
这是标准的“调度场算法”(Shunting Yard Algorithm)变体。
规则:
- 数字:直接输出(存入结果数组)。
- 左括号
(:直接入栈。 - 右括号
):不断弹出栈顶元素并输出,直到遇到左括号(为止。左括号弹出但不输出。 - 运算符
+ - \* / ^:- 栈为空或栈顶为
(时,直接入栈。 - 优先级比较:
- 如果当前运算符优先级 大于 栈顶运算符:直接入栈。
- 如果当前运算符优先级 小于或等于 栈顶运算符:
- 特例:对于乘方
^,它是右结合的(2^2^3等于2^(2^3))。如果当前是^且栈顶也是^,则不弹出,直接入栈。 - 普通情况(左结合):如果栈顶优先级
当前优先级,弹出栈顶并输出,重复比较,直到条件不满足或栈空,然后将当前运算符入栈。
- 特例:对于乘方
- 栈为空或栈顶为
优先级设定:
^: 3* /: 2+ -: 1(: 0
第二步:后缀表达式的运算与输出
题目要求输出如下格式:
8 3 2 6 * + 5 / - 4 +
8 3 12 + 5 / - 4 +
...
这意味着我们需要维护一个序列。每次找到序列中第一个出现的运算符,取出它前面的两个数字进行计算,将结果放回原位,然后输出整个序列。
策略:
- 将生成的后缀表达式存入一个
struct数组或vector中,每个节点存储它是数字还是运算符。 - 循环操作,直到数组只剩下一个元素:
- 遍历数组,找到第一个运算符。
- 取出该运算符前两个位置的数字(设为
a和b)。 - 计算
a op b。 - 修改数组:删除这三个元素(数 数 符),在原位置插入计算结果。
- 输出当前数组内容。
C++ 代码实现
#include <iostream>
#include <stack>
#include <string>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
// 定义节点,用于存储后缀表达式的元素
struct Node {
bool isOp; // 是否是运算符
long long num; // 如果是数字,存数值
char op; // 如果是运算符,存符号
};
// 获取运算符优先级
int getPriority(char op) {
if (op == '^') return 3;
if (op == '*' || op == '/') return 2;
if (op == '+' || op == '-') return 1;
return 0; // '(' 的优先级最低
}
// 执行单步运算
long long calc(long long a, long long b, char op) {
switch(op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return a / b;
case '^': return pow(a, b);
}
return 0;
}
// 打印当前的表达式序列
void printList(const vector<Node>& list) {
for (size_t i = 0; i < list.size(); i++) {
if (list[i].isOp) cout << list[i].op << " ";
else cout << list[i].num << " ";
}
cout << endl;
}
int main() {
string s;
cin >> s;
stack<char> opStack;
vector<Node> postfixList;
// --- 阶段 1: 中缀 转 后缀 ---
for (char c : s) {
if (isdigit(c)) {
// 题目保证数字只有一位,直接存入
postfixList.push_back({false, (long long)(c - '0'), 0});
} else if (c == '(') {
opStack.push(c);
} else if (c == ')') {
while (!opStack.empty() && opStack.top() != '(') {
postfixList.push_back({true, 0, opStack.top()});
opStack.pop();
}
opStack.pop(); // 弹出 '('
} else {
// 是运算符
int curPri = getPriority(c);
while (!opStack.empty()) {
char topOp = opStack.top();
int topPri = getPriority(topOp);
// 核心逻辑:
// 1. 如果栈顶优先级 > 当前,必须弹
// 2. 如果优先级相等:
// 如果是左结合 (+ - * /),要弹 (先进先算)
// 如果是右结合 (^),不弹 (后进先算)
if (topPri > curPri || (topPri == curPri && c != '^')) {
postfixList.push_back({true, 0, topOp});
opStack.pop();
} else {
break;
}
}
opStack.push(c);
}
}
// 将栈中剩余运算符弹出
while (!opStack.empty()) {
postfixList.push_back({true, 0, opStack.top()});
opStack.pop();
}
// --- 阶段 2: 运算并输出过程 ---
// 先输出最原始的后缀表达式
printList(postfixList);
while (postfixList.size() > 1) {
// 寻找第一个运算符
int idx = -1;
for (int i = 0; i < postfixList.size(); i++) {
if (postfixList[i].isOp) {
idx = i;
break;
}
}
// 找到运算符后,前面两个必定是数字 (后缀表达式特性)
long long b = postfixList[idx-1].num; // 栈顶元素 (右操作数)
long long a = postfixList[idx-2].num; // 次栈顶元素 (左操作数)
char op = postfixList[idx].op;
// 计算结果
long long res = calc(a, b, op);
// 修改列表:将 [idx-2, idx] 这三项替换为 res
// 为了方便,我们在 idx-2 处修改为结果,然后删除 idx-1 和 idx
postfixList[idx-2] = {false, res, 0};
postfixList.erase(postfixList.begin() + idx - 1, postfixList.begin() + idx + 1);
// 输出当前状态
printList(postfixList);
}
return 0;
}
关键点解析
-
右结合的处理 (^):
在中缀转后缀时,普通的 2+2+3 会变成 2 2 + 3 +(先算左边的)。
但是 223 应该变成 2 2 3 ^ ^(先算右边的)。
所以在代码的 while 循环判断优先级时,c != ‘^’ 这个条件非常关键。如果当前是 ^ 且栈顶也是 ^,我们不弹出,而是让新的 ^ 压在上面,这样出栈时新的(右边的)就会先出来。
-
为什么不用 stack 做第二步?
如果只用一个 stack
来计算,我们只能看到计算结果 9,而无法方便地打印出类似 8 3 12 + 5 / … 这种混合了“未处理后缀部分”的中间状态。使用 vector 并在中间进行替换(Erase + Insert 或原地修改)能最直观地模拟题目要求的输出过程。 -
操作数顺序:
在后缀表达式中,例如 8 3 -,3 是原本在运算符右边的数,8 是在左边的。
当我们在 vector 中找到运算符 op (位置 i) 时,i-1 是右操作数,i-2 是左操作数。计算时要是 (i-2) op (i-1)。这一步特别是做除法和减法时绝对不能反。