赞
踩
中缀表达式是最常用的算术表达式形式——运算符在运算数中间。但运算时需要考虑运算符优先级。
后缀表达式是计算机容易运算的表达式,运算符在运算数后面,从左到右进行运算,无需考虑优先级,运算呈线性结构。
1 + 2 * 3// 中缀表达式
1 2 3 * +// 后缀表达式
计算机直接处理中缀表达式是比较困难的,以表达式1 + 2 * 3
为例:当计算机读取到1 + 2后就可以直接得出结果3了吗?答案是否定的,因为后面可能会有优先级更高的运算符。
就拿2来说,究竟是先和左操作数计算还是先与右操作数计算?这才是解决这类问题的核心与本质。
后缀表达式也叫逆波兰表达式,我们可以通过以下的方法将中缀表达式转化为后缀表达式(我们用一个栈来临时存储中间遇到的操作符):
遇到 操作数,直接存储/输出
遇到 操作符
- 栈为空或者该操作符的优先级比栈顶的操作符的优先级高 → 将该操作符压栈
- 该操作符的优先级比栈顶的操作符优先级低 or 相同 → 弹出栈顶操作符存储,并将该操作符压栈
遍历结束后将栈里的操作符依次全部弹出,依次存储在末尾(或者直接输出)
我们以 n1 <O1> n2 <O2> n3 <O3> n4……
为例加以说明,其中 ni 表示操作数,Oi 表示操作符。
(1)如果 O1 的优先级高于 O2,那么毫无疑问,n2 会先与左操作数运算。
(2)如果 O1 的优先级低于 O2,那么可以让 n2 与右操作数 n3 发生运算吗?答案是否定的!因为n3 能不能先与 n2 运算还取决于 O2 O3 运算符优先级的高低。但是目前我们可以确定 O2 运算符肯定在 O1 运算符前被使用。
O1 操作符先进入,但是在情况二中因为优先级低,只能后运算。能够存储这种先进后出模式的数据结构,栈自然是不二之选!!
1) 对于括号怎么处理?
从上面的学习中我们不难发现,先出栈的运算符一定比后出栈的运算符先运算。而括号作为优先级最高的运算符,我们如何体现出它的“优越性”呢?
其实处理的办法很简单,进入左括号后遇到的第一个运算符无脑压栈,不管优先级高低。这样就可以保证优先运算。
其实可以将括号里的式子看成一个个独立的式子,式子的结束就是遇到右括号的时候。在结束时,我们需要把这个独立子式中的操作符依次全部弹出(左括号之上的操作符,之下的可不归你管)
2) 对于负数怎么处理?
负数会在什么情况下出现呢? 我们不妨简化一下问题,对于一个标准的式子来说:
-1 + 5 * 3
;3 + (-6) / 2
;我想到了两种思路来处理:
-
出现在式子的开头,我们则认为其代表负号而非减号这段代码中采用思路一,即需要根据 -
出现的位置判断究竟是减号还是负号。好处是我们便于得到中间的结果——后缀表达式,如果不需要这个结果只是想计算一个中缀表达式最后的结果,推荐参考文章最后一部分中缀表达式直接求解的部分,思路上更加清晰
#include <iostream> #include <stack> #include <vector> #include <string> #define cmp(c) (c == '*' || c == '/')? (1) : (0) using namespace std; class calc { public: // 中缀表达式转后缀 void infix2Postfix(const string& s) { int n = s.size(); char top_op; // 用来临时存放栈顶的元素 for (int i = 0; i < n; ++i) { if (s[i] == ' ') continue; if (first && s[i] == '-') { // 作为负号而非减号,不需要后续的压栈处理 sign = -1; first = false; continue; } first = false; if (isdigit(s[i])) { // 如果遇到数字,计算后直接存储 int num = s[i] - '0'; while (i + 1 < n && isdigit(s[i + 1])) { num = num * 10 + (s[++i] - '0'); } output.push_back(to_string(num * sign)); sign = 1; } else if (s[i] == '(') { // 遇到左括号,遇到的下一个 op 可以无脑压栈 first = true; priority = true; op.push('('); } else if (s[i] == ')') { priority = false; // 避免 () 中没有运算符的情况,这样 priority 就带出到了括号外 while (op.top() != '(') { // 将 `()` 中的运算符出栈 top_op = op.top(); output.emplace_back(1, top_op); op.pop(); } op.pop(); // 最后弹出栈顶的失效的 '(' } else { if (op.empty() || priority) { // ‘(’ 的最高优先级就是由 priority保证的 op.push(s[i]); priority = false; } else { top_op = op.top(); if (cmp(top_op) >= cmp(s[i])) { output.emplace_back(1, top_op); op.pop(); } op.push(s[i]); } } } while (!op.empty()) { // 将剩余的操作符依次全部弹出 top_op = op.top(); output.emplace_back(1, top_op); op.pop(); } for (const auto& e : output) { cout << e << " "; } cout << endl; } private: int sign = 1; // 代表数字的正负号 bool first = true; // 如果 '-' 作为第一个字符出现,那么将其视为负号而非减号 bool priority = false; // 进入 ( 的第一个运算符具有压栈的最高优先级 vector<string> output; // 存储“输出”的结果 stack<char> op; // 用栈保存操作符 };
转化为后缀表达式后,我们就不需要考虑运算符优先级的高低,利用下面的方法就可以求出最终的结果:
遇到操作数 → 直接入栈
遇到操作符 → 取出栈顶的两个数据进行运算,将计算结果压栈
(注意:转换过程中操作数的相对位置始终没有发生改变,所以栈最顶上的是右操作数,它下面的才是左操作数)
class calc { public: void infix2Postfix(const string& s) { // 略…… } int calcPostfix() { stack<int> res; for (const auto& token : output) { if (isNumber(token)) { res.push(atoi(token.c_str())); } else { int right = res.top(); res.pop(); int left = res.top(); res.pop(); switch (token[0]) { case '+': res.push(left + right); break; case '-': res.push(left - right); break; case '*': res.push(left * right); break; case '/': res.push(left / right); break; } } } return res.top(); } private: bool isNumber(const string& s) { return !(s == "+" || s == "-" || s == "*" || s == "/"); } private: // 成员变量略…… };
这里采用减法来模拟负数,从而避免了 -
是减号还是符号的苦恼,从而让代码在形式上更加统一,思路上也更加简单
inline int priority(char c) { return c == '*' || c == '/' ? (1) : (0); } class calc { public: // 例:s = “1 + 6 / 3” int eval(const string& s) { int sz = s.size(); if (s[0] == '-') //[解释]:对负数的处理,通过减法来模拟负数 num.push(0); for (int i = 0; i < sz; ++i) { if (s[i] == ' ') continue; else if (isdigit(s[i])) //[作用]:字符串转数字的操作 { int n = s[i] - '0'; while (i + 1 < sz && isdigit(s[i + 1])) n = n * 10 + (s[++i] - '0'); num.push(n); } else if (s[i] == '(') { op.push('('); flag = true;//[解释]:标记进入左括号,下一个 op 无脑压栈 if (s[i + 1] == '-') num.push(0); } else if (s[i] == ')') { flag = false; // [注意]:在遇到左括号后的第一个运算符后,flag才会被重置为 // false,但遇到括号里无运算符的情况就可能会出错,所以加个双保险 char _operator = op.top(); while (_operator != '(') { // [解释]:弹出运算符进行运算的过程 count_push(_operator); op.pop(); _operator = op.top(); } op.pop(); // 把'('弹出来 } else { if (op.empty() || flag) { op.push(s[i]); flag = false; } else { /** * 注意,这里必须采用 while 而非 if,因为同运算级的操作符必须从左往右 * 使用 if 每次只是弹出一个操作符,这可能会导致同级运算符从右往左计算的出现 * 例如 0-5*6+(-1) -> 0-30+(-1), 如果 - 不立即与 30 运算,那们 + 就会 * 压入栈顶,变成了 0-(30-1) */ while (!op.empty() && priority(op.top()) >= priority(s[i])) { if (op.top() == '(') break; // ( 不参与运算,只作为限界符 count_push(op.top()); op.pop(); } op.push(s[i]); } } } while (!op.empty()) //[解释]:遍历结束后将栈里的操作符依次全部弹出 { count_push(op.top()); op.pop(); } return num.top(); } private: //[作用]:进行栈顶两个数据的运算并压入栈中 void count_push(char _operator) { int tmp = 0; int right = num.top(); num.pop(); int left = num.top(); num.pop(); switch (_operator) { case '+': tmp = left + right; break; case '-': tmp = left - right; break; case '*': tmp = left * right; break; case '/': tmp = left / right; break; } num.push(tmp); } private: bool flag = false; //[作用]:用来标记是否进入左括号 bool first = true; //[作用]:用来标记是否是 stack<int> num; //[作用]:存储数据的栈 stack<char> op; //[作用]:存储运算符的栈 };
同时感谢评论区的大神指出这段代码原先的 bug 并提出简洁优雅的解决方案。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。