(三)后缀表达式
爸爸新买了一个全新的科学计算器,功能非常强大,相比之下子渊的那个玩具一样的简易计算器就逊色多了。最让子渊惊奇的是科学计算器能够一次性接受一连串的表达式,然后飞快地给出结果,而他的简易计算器却每次只能接受一个操作数或者一个操作符,麻烦死了。
比如计算 200805 + 30 * 350,用简易计算器只能先输入操作数“30”,然后输入操作符“*”,再输入操作数“350”,得到中间运算结果“10500”,再输入操作符“+”,最后输入操作数“200805”,点击“=”,才能得到最终结果“211305”。
而科学计算器却只要直接输入“200805 + 30 * 350”,然后点击“=”,就能得到最终结果“211305”。
简直太神奇了!它是怎么做到的呢?
子渊带着这个问题来到爸爸的书房。爸爸正在下围棋忙得不亦乐乎,听子渊说完以后,头也没回,只丢下一句话:“不好意思啊我现在没空!再等两分钟吧。顺便说一句,这个问题用我昨天跟你讲的数据类型栈可以很好的解决,你自己先想想看吧。呵呵,杀你大龙!”
用栈解决?怎么看也搭不上边啊!子渊陷入沉思中。
“终于赢了!”两分钟后,爸爸大吼一声,满脸兴奋地转过身来,“想出来没有啊?”
“一点思路也没有,简直是乱说嘛!”
“乱说?我可没有乱说啊!哦,可能跨度有点大,你先解决这道题目吧。”爸爸从他的习题库里调出了一道题目:
标准的表达式如"A+B",在数学上学名叫中缀表达式,原因是运算符号在两个运算对象的中间。相对应的还有前缀表达式,如:"+ - A * B C D",转换成中缀表达式为:"A - B * C + D";后缀表达式,比如前所述的中缀表达式转换为后缀表达式为:"A B C * - D +"。为了纪念波兰数学家鲁卡谢维奇(Jan Lukasiewicz),前缀表达式被称作波兰表达式,后缀表达式称为逆波兰表达式。
逆波兰表达式是一种把运算符后置的算术表达式,例如普通的表达式2 + 3的逆波兰表示法为2 3 +。逆波兰表达式的优点是运算符之间不必有优先级关系,也不必用括号改变运算次序,运算符放在两个运算对象之后,所有计算按运算符出现的顺序,严格地由左往右进行。例如5 * (7 – 3) + 9对应的逆波兰表达式为5@7@3@ - * 9@ + #,其中#为逆波兰表达式的结束标志,@为操作数的结束符。
计算过程如下:
5@7@3@ - * 9@ +
= 5@4@ * 9@ +
= 20@ 9@ +
= 29
本题要求计算逆波兰表达式的值,为简单起见,假设操作数为正整数,运算符只包括+ - * / 四个。(有创造力的读者可以自己提升难度:拓宽操作数的类型,增加运算符的总类)
设计一个算法来实现该功能,子函数接口为:
FUNCTION ComputerReversePolishNotation(rpn : string) : integer;
子渊一看,发现这道题目简单无比,只要一个入栈和出栈就可以搞定。由左往右处理字符串的每一个字符,若遇到操作数就保存到栈中;若遇到操作符就从栈中弹出栈顶的两个操作数进行计算,然后将结果重新压入栈中。依此类推,直到表达式最后一个操作符处理完毕,这时的栈顶元素值即为计算结果。
子渊稍作分析,便得知了代码的写法,但是他想:每次都是爸爸考我,也不知道他自己水平如何,不如就用这道题目考考他吧,呵呵!
眉头一皱,计上心来:“爸爸,你每次都是给我提示,从来没有在我面前写过完整的代码,不如这次我们每人都写一个,比比看谁的好吧?”
“小家伙,想考我啊!那就让你见识见识。好好看着哦!”爸爸心想这道题目确实不难,就从另外的角度让子渊学习学习编写代码的方法吧,于是满口答应。
不到五分钟,代码编辑,编译,运行成功:
{代码6:}
{计算逆波兰表达式的值}
PROGRAM ReversePolishNotation(INPUT, OUTPUT);
CONST
MAXCAPACITY = 255; {栈的最大容量}
TYPE
ElemType = integer; {栈内元素数据类型}
Stack = array [1..MAXCAPACITY] of ElemType; {用数组表示的栈}
VAR
rpn : string;
s : Stack; {定义s为栈}
top : integer; {栈顶标志}
PROCEDURE Push(var s : Stack; var top : integer; data : ElemType); {入栈}
begin
if top = MAXCAPACITY then
writeln('overflow')
else
begin
inc(top);
s[top] := data;
end; {if}
end; {Push}
FUNCTION PopTop(s : Stack; var top : integer) : ElemType; {获取栈顶元素的值并退栈}
begin
if top = 0 then
writeln('underflow')
else
begin
PopTop := s[top];
dec(top);
end; {else}
end; {PopTop}
FUNCTION ComputerReversePolishNotation(rpn : string) : integer;
var
i, value : integer;
begin
i := 1;
while rpn[i] <> '#' do
begin
case rpn[i] of
'0'..'9' : begin
value := 0;
repeat {从后缀表达式中取出操作数}
value := 10 * value + ord(rpn[i]) - ord('0');
inc(i);
until rpn[i] = '@';
Push(s, top, value); {操作数入栈}
end; {'0'..'9'}
'+' : Push(s, top, PopTop(s, top)+PopTop(s, top)); {弹出两个操作数并做加法运算}
'-' : begin
value := PopTop(s, top); {弹出减数}
Push(s, top, PopTop(s, top)-value); {弹出被减数并做减法运算}
end; {'-'}
'*' : Push(s, top, PopTop(s, top)*PopTop(s, top)); {弹出两个操作数并做乘法运算}
'/' : begin
value := PopTop(s, top); {弹出除数}
if value = 0 then {预防除数为0的情况}
begin
writeln('divisor = 0');
exit;
end; {if}
Push(s, top, PopTop(s, top) div value); {弹出被除数并做除法运算}
end; {'/'}
end; {case}
inc(i);
end; {while}
ComputerReversePolishNotation := PopTop(s, top); {返回运算结果}
end; {ComputerReversePolishNotation}
BEGIN {MAIN}
top := 0; {栈顶初始化}
writeln('Input rpn:');
readln(rpn);
writeln(ComputerReversePolishNotation(rpn));
END.
子渊一看代码,觉得有些不对劲:“不对啊!爸爸,你的代码里怎么只有一个入栈基本运算啊?还有那个PopTop函数是怎么回事?”
“傻眼了吧!告诉你:前面的题目中我们直接调用栈的基本操作,是为了让你熟悉栈的特点。但是,我们学算法不应拘泥于形式,而要领会其精神,灵活运用栈的特点编写更加短小精悍的代码。
上面的代码虽然没有明显地写出栈的各种基本操作,但实际上已经暗含在各个语句中了,PopTop函数实际上是Pop和GetTop函数的综合。
实际上,在以后的代码中,也许你根本看不到Push,Pop和GetTop的字样,但是,只要题目中有可以表示栈的数组或链表,有后进先出的特征呈现,它就是栈。
栈的应用非常广阔,在以后学习的深度优先搜索,回溯等知识时,栈还要充当重要的主角,希望你能够掌握栈的精髓,而不仅仅是几个函数。
接下来是与上一题密切相关的一个题目,解决了它,科学计算器的奥秘就不神秘了。如何灵活地运用栈,使代码更简洁,美观,你要好好想一想:
将一个普通的中序表达式转换为逆波兰表达式。例如中序表达式5 * (7 – 3) + 9对应的逆波兰表达式为5@7@3@ - * 9@ + #,其中#为逆波兰表达式的结束标志,@为操作数的结束符。
为简单起见,假设操作数为正整数,只出现小括号,并且运算符只包括+ - * / 四个。(有创造力的读者可以自己提升难度:拓宽操作数的类型,增加运算符的总类)
子过程接口为:
PROCEDURE CreateReversePolishNotation(var rpn : string; infix : string);
逆波兰表达式把运算符后置,也就是说我先要把运算符后面的操作数读出来,再去处理运算符,这样就需要先把运算符存储起来。那存到哪里呢?当然是栈中了。
逆波兰表达式没有括号和优先级关系,那就完全由运算符的出栈顺序来决定计算顺序,优先级别越高的运算符越先出栈。
那在运算符入栈时就要考虑栈顶运算符的优先级别情况:如果栈顶运算符优先级低于该运算符,则将该运算符入栈;否则将栈顶运算符从栈中弹出,直到栈顶运算符的优先级低于当前运算符为止,将该运算符入栈(左右括号是特例)。
在本题中,优先级别的设定是大家都熟知的:’*’,’/’最高;其次为’+’,’-‘;再次为左,右括号’(‘和’)’; 最后为逆波兰表达式的结束标志’#’。
有了这些算法分析,答案就呼之欲出了:
{代码7:}
{将中序表达式转换为逆波兰表达式}
PROGRAM ReversePolishNotation(INPUT, OUTPUT);
VAR
rpn, infixNotation : string; {前缀,后缀表达式}
{返回各种操作符的优先级别}
FUNCTION Priority(ch : char) : integer;
begin
case ch of
'#' : Priority := 0;
'(',')' : Priority := 1;
'+','-' : Priority := 2;
'*','/' : Priority := 3;
end; {case}
end; {Priority}
PROCEDURE CreateReversePolishNotation(var rpn : string; infix : string);
var
s : array [1..255] of char; {定义s为栈}
top : integer; {栈顶标志}
i, len : integer;
begin
top := 1; {栈顶初始化}
s[top] := '#';
len := length(infix);
for i:=1 to len do
begin
if infix[i] in ['(',')','+','-','*','/'] then
begin
if infix[i-1] in ['0'..'9'] then {操作数结束标志}
rpn := rpn + '@';
if (infix[i] = '(') or (Priority(infix[i]) > Priority(s[top])) then {入栈}
begin
inc(top);
s[top] := infix[i];
end {if}
else if infix[i] = ')' then
begin
while s[top] <> '(' do {退栈,直到遇到左括号为止}
begin
rpn := rpn + s[top];
dec(top);
end; {while}
dec(top); {左括号出栈}
end {else if}
else {栈顶运算符的优先级不低于当前运算符,先退栈,再入栈}
begin
while Priority(infix[i]) <= Priority(s[top]) do
begin
rpn := rpn + s[top];
dec(top);
end; {while}
inc(top);
s[top] := infix[i];
end; {else}
end {if}
else {读入操作数}
rpn := rpn + infix[i];
end; {for}
if infix[len] in ['0'..'9'] then {操作数结束标志}
rpn := rpn + '@';
while top > 0 do {将栈内所有元素弹出}
begin
rpn := rpn + s[top];
dec(top);
end; {while}
end; {CreateReversePolishNotation}
BEGIN {MAIN}
writeln('Input infixNotation:');
readln(infixNotation);
CreateReversePolishNotation(rpn, infixNotation);
writeln(rpn);
END.
看了子渊的代码,爸爸忍不住笑了起来:“小家伙,领悟挺快的啊,对栈的基本思想已经有了一定的认识了!好了,把上述两个子函数结合起来,去实现你的混合运算吧。”