一、引入
何为逆波兰表达式(后缀表达式)?
我们先来看一个例子:
这是一个我们最熟悉的算数式:(a+b)*c-(a+b)/e
我们知道的是,在计算上述算式时,需要将(a+b)*c与(a+b)/e相减,而被减数与减数在计算时又要先算括号里的,再算乘除法,这是我们所习惯的,但是如果把整个算式交给计算机,计算机不会知道先括号,再乘除,再加减,他只会按照顺序读下去,那么计算机是如何进行这样的算式的计算呢?我们在这里可以模拟一下计算机,既然他只能按顺序读,那我可以把上面的算式做一下变形,把式子拿下来:
(a+b)*c-(a+b)/e
我们观察到,这个算式本质上是个减法,也就是最后算的其实是一个减法,那么我们把减号放到最后,等计算机读到最后再去处理,也就是这样:
((a+b)*c)((a+b)/e)-
在两个部分中,本质上是计算乘法,也就是最后计算的是乘法,那就把乘号放在每个部分的最后,等计算机计算完前面的再处理乘法,像这样:
((a+b)c*)((a+b)e/)-
在每个最小的括号里,实际上算的是加法:a+b,按照刚才的操作,本质上计算的是加法,那就把加号放到最后,等到计算机计算完前面的算式再相加,像这样:
((ab+)c*)((ab+)e/)-
然后我们发现,没有更小的算式可以拆解了,也就是我们精确到了算式每个数字,那就可以停止啦,毕竟分不下去了,再来观察这个式子,发现其保留了最开始的计算优先级,并且是按照从左到右的顺序让计算机依次执行的,故此表达式为我们所想要的--能交给计算机去执行的表达式。再次观察发现,前面的算式优先级最高,其实我们可以将前面的括号去掉,因为不需要用括号去保证计算的优先级了,像这样:
ab+c*ab+e/-
好啦,最终得到这个表达式,我们就叫做逆波兰表达式,也叫后缀表达式,相比于一开始的算式,后缀表达式是符合计算机的习惯的,而最开始给出的、我们所熟知的表达式,操作符都在两个操作数中间,我们称为中缀表达式。这样再回头看这个后缀表达式,我们发现每次计算都有两个操作数跟着一个操作符,只不过优先级最高的在最前面,同时,同等级的计算要分别进行,最后相减。
既然都说了后和中,相比大家也好奇前缀表达式吧,其实后缀表达式和前缀表达式是一样的,只不过是把操作符放在两个操作数前面,例如:1 - (3 + 5) 写成 - 1 + 3 5。而后缀表达式其实是由前缀表达式得出的,具体是怎样得出的呢? 我们看例子不难发现,前缀表达式如果想要让计算机顺序处理,是要从后往前读的,而后缀表达式更符合人们的习惯,故至今为止,使用较多的仍为后缀表达式。
至于为什么叫逆波兰表达式,实际上这种表达方式最早是由波兰的数学家提出的,前缀表达式称为波兰表达式,而后缀表达式为了区分前缀表达式,也契合特点,就叫逆波兰表达式啦。
二、算法分析与过程
严格来说不应该叫算法分析,应该叫需求分析,但是无所谓啦,我们的目的是把我们所熟悉的中缀表达式转化为后缀表达式,这里依然借助引入中的例子(这个例子来源于百度百科):
(a+b)*c-(a+b)/e ---> ab+c*ab+e/-
观察发现,所有操作数的相对顺序都没变,变的只是运算符。
我们在学习数据结构这门课时,只要学到栈,老师一定会举的例子就是后缀表达式,他可以利用栈这种数据结构来实现,因为涉及到一个运算符的优先级的问题,如果对栈有深入了解的同学,相信大家会发现这个很像单调栈的问题,此处不再赘述。
我们来模拟一下这个过程,首先有两个栈S1和S2,S2负责保存最终的表达式结果,S1负责临时保存运算符(左侧为栈底,右侧为栈顶):
初始状态:
S1 : #
S2 :
S1中的'#'看成运算优先级最低的符号,方便我们后续操作;
依次扫描中缀表达式,如果是数字,直接压入S2;如果是操作符,如果当前S1栈顶操作符的优先级低于取到的运算符,直接把当前运算符压入S1,否则将S1栈顶元素弹出并压入S2,直至S1栈顶运算符优先级比自己低为止;如果是左括号,直接压入S1;如果是右括号,将S1元素出栈并入栈S2,直至遇到第一个左括号为止,并抛弃这个左括号。重复进行直至完成。
我们从初始状态来模拟(每一步代表从左到右依次取到一个字符):
1. S1 : # (
S2 :
2. S1 : # (
S2 : a
3. S1 : # ( +
S2 : a
4. S1 : # ( +
S2 : a b
5. S1 : # (此时遇到了右括号,将两括号之间的运算符弹出并压入S2中,并抛弃左括号)
S2 : a b +
6. S1 : # *
S2 : a b +
7. S1 : # *
S2 : a b + c
8. S1 : # - (此时遇到减号,优先级低于栈顶的*,将*弹出并压入S2)
S2 : a b + c *
9. S1 : # - (
S2 : a b + c *
10. S1 : # - (
S2 : a b + c * a
11. S1 : # - ( +
S2 : a b + c * a b
12. S1 : # -(此时遇到了右括号,将两括号之间的运算符弹出并压入S2中,并抛弃左括号)
S2 : a b + c * a b +
13. S1 : # - /
S2 : a b + c * a b +
14. S1 : # - /
S2 : a b + c * a b + e
15. S1 : # (此处中缀表达式取完,将S1的元素依次出栈至只剩下'#')
S2 : a b + c * a b + e / -
好了,到此为止,S2中的顺序即是我们所需要的后缀表达式,如果我们想要计算最后的结果,我们就在压入S2的过程中计算,每次压入S2一个操作符,我们先不压入,而是取S2栈顶的两个数字进行该运算符的运算,再将结果压入S2中。都操作完成后,S2中剩下的数字即为最终的结果。
这里留下一个思考问题,如果我就想输出后缀表达式,应该怎么做呢?
三、代码实现
此处为C++代码
#include <iostream>
#include <string>
#include <unordered_map>
#include <stack>
#include <queue>
using namespace std;
string infix; //中缀表达式用作输入
string postfix; //后缀表达式用作输出
int ans; //用于记录算式的结果
stack<char> s1; //运算符栈
stack<int> s2; //运算数栈
unordered_map<char, int> mp; //制定各个运算符的优先级,注意这里左右括号要置于'#'和'+'之间
queue<char> q; //用于保存入s2的顺序,实际为逆波兰表达式,解决了刚刚的思考问题
int postfixvalue(string infix)
{
for (char c : infix)
{
//左括号直接压入s1
if (c == '(')
{
s1.push(c);
}
//右括号,将s1栈顶最近的左括号上面的运算符弹出并进行运算,还要将左括号出栈
else if (c == ')')
{
while (s1.top() != '(')
{
char ch = s1.top();
q.push(ch);
s1.pop();
int a = s2.top();
s2.pop();
int b = s2.top();
s2.pop();
if (ch == '+')
{
s2.push(a + b);
}
else if (ch == '-')
{
s2.push(a - b);
}
else if (ch == '*')
{
s2.push(a * b);
}
else if (ch == '/')
{
s2.push(a / b);
}
}
s1.pop();
}
//数字直接压入s2
else if (c <= '9' && c >= '0')
{
s2.push(c - '0');
q.push(c);
}
//运算符要判断优先级,小于等于s1栈顶优先级则让s1出栈做运算,直至s1栈顶优先级小于其为止
else
{
if (mp[c] > mp[s1.top()])
{
s1.push(c);
}
else
{
while (mp[c] <= mp[s1.top()])
{
char ch = s1.top();
q.push(ch);
s1.pop();
int a = s2.top();
s2.pop();
int b = s2.top();
s2.pop();
if (ch == '+')
{
s2.push(a + b);
}
else if (ch == '-')
{
s2.push(a - b);
}
else if (ch == '*')
{
s2.push(a * b);
}
else if (ch == '/')
{
s2.push(a / b);
}
}
s1.push(c);
}
}
}
//由于最后一定是数字或者右括号,故s1中可能有剩余的运算符残留,将其一一出栈并运算
while (s1.top() != '#')
{
char ch = s1.top();
q.push(ch);
s1.pop();
int a = s2.top();
s2.pop();
int b = s2.top();
s2.pop();
if (ch == '+')
{
s2.push(a + b);
}
else if (ch == '-')
{
//想想这里为什么是b-a而不是a-b?
s2.push(b - a);
}
else if (ch == '*')
{
s2.push(a * b);
}
else if (ch == '/')
{
//想想这里为什么是b/a而不是a/b?
s2.push(b / a);
}
}
//s2中剩下的操作数即为运算结果
return s2.top();
}
int main()
{
cout << "请输入中缀表达式:";
cin >> infix;
s1.push('#');
mp.insert(make_pair('#', -1));
mp.insert(make_pair('(', 0));
mp.insert(make_pair('+', 1));
mp.insert(make_pair('-', 1));
mp.insert(make_pair('*', 2));
mp.insert(make_pair('/', 2));
int ans = postfixvalue(infix);
cout << "运算结果为:" << ans << endl;
cout << "后缀表达式为:";
while(!q.empty())
{
char ch = q.front();
q.pop();
cout << ch;
}
cout << endl;
return 0;
}
仔细看过代码会发现,此代码仅仅可做一位数的运算,且不能输出字母公式(如引入中百度百科那个例子), 如何进行拓展,有很多方式,感兴趣的读者可以自行实现。
四、总结
本篇文章主要介绍了对栈这种数据结构的一个典型应用--逆波兰表达式,通过一个运算符优先级严格递增的单调栈,来进行计算顺序的确定,同时也谈到了计算机处理算式与人类的不同,以及人们如何让我们熟悉的中缀表达式转化为计算机可以计算的后缀表达式。
篇幅和笔力有限,不能更加完美的阐述逆波兰表达式,同时如有疑问以及错误,欢迎随时提出!