「栈」的应用系列之「表达式求值」

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/young2415/article/details/85120406

说到表达式求值,我不禁想起了我大二那会儿,刚刚开始自学Java,正好我女朋友她们专业那学期也开了 Java 这门课,期末的时候,老师布置了一个作业,让她们用 Java 做一个小东西,计算器、日历、记事本都可以,我女朋友的作业自然落到了我头上。这可是展示我大好才华的机会啊,于是我雄心勃勃地想要做一个计算器出来。

刚开始很顺利,图形界面很快就做出来了,结果到了核心部分,也就是计算表达式的时候,我绞尽了脑汁,怎么也写不出一个完美的算法来计算用户输入的表达式。最后没办法,只实现了一个简单的功能,就是只能计算「34 + 76」或者「3 × 4」这种简单的只有两个数的表达式。

虽然最后女朋友拿着这个半成品也顺利地通过了老师检查,但是我不甘心呐!自己私下里苦苦思索了好久,直到我学习了数据结构,才知道,原来栈可以完美地解决表达式求值的问题,无论你的表达式有多复杂,在栈的面前,都会黯然失色,俯首称臣。

所以说,读者朋友们,千万要学好数据结构与算法啊!不要到了该装逼的时候却后悔莫及!前辈们总是说「数据结构与算法是程序员的内功」,这话说的一点没错!那么今天我们就来看一下,如何利用栈来对表达式进行求值。

一、逆波兰表达式

逆波兰表达式(reverse Polish notation, RPN)又叫做后缀表达式,波兰逻辑学家 J.Lukasiewicz 于1929年提出了这种表示表达式的方法,按此方法,每一运算符都置于其运算对象之后,故称为后缀表达式。与此相对应的,我们将我们平常所见到的将二元运算符置于与之相关的两个运算对象之间的表达式称作中缀表达式。

举个例子大家就明白了,「1 + 2」就是中缀表达式,若把加号写在最后面:「1 2 +」就是后缀表达式。再来一个复杂点的,中缀表达式「 (1 + 2) * 3 + 4」写成后缀表达式就是「 1 2 + 3 * 4 +」。如果读者对于逆波兰表达式不理解,可以参考维基百科对其的解释:维基百科 - 逆波兰表达式

这种表示方式简直就是反人类的,为什么呢?因为很容易引起歧义,比如「123+」,我怎么知道这表示的是「1 + 23」还是「12 + 3」?即使是在 12 和 3 之间加一个空格,来让「12 3 +」表示「12 + 3」,但是对我们人类来说还是不够直观。所以这种表示方式在日常生活中并不常见。但是从计算机的角度看,后缀表达式处理起来会更加简便,因为计算机只要自左向右扫描后缀表达式,就可以完成表达式的求值。具体过程如下:

当遇到操作数时,则将该操作数进栈;

当遇到操作符时,则令栈中的操作数出栈,计算完之后将计算结果进栈,继续向右扫描。

当扫描到表达式的结尾时,此时栈顶元素存放的就是最终的计算结果。

二、调度场算法

我们既然知道了计算机算后缀表达式的时候贼 6,那么现在的问题是,如何把我们所熟知的中缀表达式转换成后缀表达式呢?因为计算程序最终是要面向普通用户的啊,我们总不能在用户使用前还得交他们使用后缀表达式吧,这显然是不现实的。所以需要用户输入中缀表达式然后程序自动转换成后缀表达式。

这时我们需要用到大神迪杰斯特拉(Edsger Wybe Dijkstra)发明的「调度场算法(Shunting-yard algorithm)」,之所称之为调度场算法,是因为它的操作类似于火车的调度。我们来看一下具体操作步骤。

假设初始时,有一个合法的中缀表达式 Input,那么调度场算法需要一个空的栈,以及一个空的序列 Output,该算法每次处理中缀表达式中的一个元素。

如果遇到的是一个数,那么就把这个数追加到 Output 的末尾;

如果遇到的是操作符,就将该操作符与栈顶操作符相比较,若当前操作符的优先级高于栈顶操作符的优先级,则将当前操作符推进栈中;如果当前操作符的优先级小于等于栈顶操作符的优先级,则弹出栈顶操作符,并将弹出的栈顶操作符追加至 Output 的末尾,继续拿当前操作符与新的栈顶操作符相比较。

直到 Input 被扫描完毕,最后将栈中仍有的操作符弹出并追加至 Output 末尾,这时 Output 就是最终的结果。

为了这个算法能够顺利执行,需要有一个运算符优先级表供算法在执行过程中查阅,内容如下:

运算符优先级表

该表的使用方法是,假设当前运算符是「+」,栈顶运算符是「*」,则看第三行第一列,是「>」,说明栈顶运算符的优先级大于当前操作符的优先级。为了统一算法的处理流程,将左右括号以及标识表达式尾部的字符 '\0' 也当做操作符来看待,初始时先将空字符入栈,它的优先级是最低的。

以「3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3」为例,将其转换成后缀表达式的过程如下:

符号 动作 输出(RPN) 操作符栈 备注
空字符 '\0' 入栈 \0
3 追加至 RPN 末尾 3 \0
+ 入栈 3 + \0
4 追加至 RPN 末尾 3 4 + \0
* 入栈 3 4 * + \0 「*」的优先级比「+」高
2 追加至 RPN 末尾 3 4 2 * + \0
/ 「*」出栈,追加至 RPN 末尾 3 4 2 * + \0 「/」和「*」的优先级相同
「/」入栈 3 4 2 * / + \0 「/」的优先级比「+」高
( 入栈 3 4 2 * ( / + \0
1 追加至 RPN 末尾 3 4 2 * 1 ( / + \0
- 入栈 3 4 2 * 1 - ( / + \0
5 追加至 RPN 末尾 3 4 2 * 1 5 - ( / + \0
) 「-」出栈,追加至 RPN 末尾 3 4 2 * 1 5 - ( / + \0 「)」的优先级比「-」低
「)」出栈 3 4 2 * 1 5 - / + \0 丢弃括号
^ 入栈 3 4 2 * 1 5 - ^ / + \0
2 追加至 RPN 末尾 3 4 2 * 1 5 - 2 ^ / + \0
^ 「^」出栈,追加至 RPN 末尾 3 4 2 * 1 5 - 2 ^ / + \0
「^」入栈 3 4 2 * 1 5 - 2 ^ ^ / + \0
3 追加至 RPN 末尾 3 4 2 * 1 5 - 2 ^ 3 ^ / + \0
\0 将栈中的运算符全部弹出,依次追加至RPN末尾 3 4 2 * 1 5 - 2 ^ 3 ^ / +

调度场算法图示 / 维基百科

三、结语

当用户输入一个中缀表达式时,我们的处理主要分为两步,第一步利用调度场算法将中缀表达式转换成后缀表达式(或叫做逆波兰表达式),第二步再将逆波兰表达式求值,就可以得到最终的结果,这两个步骤都用到了「栈」这个数据结构。由于篇幅所限,本文没有贴出代码。但是代码已经上传至 GitHub,读者可以点击这里下载。

到这,相信大家已经见识到了数据结构的威力,合理地利用数据结构,我们可以简化解决问题的步骤,并能够有效地减少时间和空间复杂度。我们这篇文章讲的是利用栈实现计算器的功能,这个计算器可以解析复杂的表达式并计算出结果。事实上,栈可以做的事情远不止于此,利用栈我们还可以将递归的算法改写成非递归的算法,从而降低内存的消耗。以后有机会再给各位读者朋友详细介绍。

由于本人水平有限,有些地方可能讲解的不太清楚,如果您有不明白的地方,欢迎私信吐槽,提出您宝贵的建议。希望我们能共同进步!

欢迎关注我的微信公众号,扫描下方二维码或微信搜索:AProgrammer,就可以找到我,我会持续为你分享 IT 技术。

猜你喜欢

转载自blog.csdn.net/young2415/article/details/85120406