中缀表达式转换成后缀表达式的算法理解
【2012统考真题】已知操作符包括“+”、“-”、“*”、“/”、“(”和“)”。将中缀表达式a+b-a*((c+d)/e-f)+g
转换为等价的后缀表达式ab+acd+e/f-*-g+
时,用栈来存放暂时还不能确定运算次序的操作符。若栈初始时为空,则转换过程中同时保存在栈中的操作符的最大个数是
liuyubobobo 老师曾表达一个观点,学算法,光能够体会算法的精妙之处本来就是值得兴奋的事,若能将算法思想用于自己的工作中则更是难得的美事。
1. 概念
1.1 宏观概念
我们书写出一个含加减乘除等运算符的数学计算过程,我们能借助草稿纸得到答案。如以下的式子1 + 2 - 3 * ( (4 + 5) / 6 - 7 ) + 8
,我们根据运算符的常识就能模拟整个运算过程。但是计算机要怎么读这一个字符串,并输出跟人类在草稿纸上计算一样的结果呢?
伟大的计算机科学家发明了通俗易懂的算法,该算法能分成两个子算法:
人类运算语言 -> 计算机整理后的运算数据
和 计算机整理后的运算数据 -> 计算机运算
。
其中 人类运算语言 -> 计算机整理后的运算数据
== 中缀表达式 -> 后缀表达式
计算机先将人类的数学语言翻译成方便计算机运算的数据,表面上看,会将原有字符串中的括号删除(蕴含着匹配的逻辑)。这也能解释括号匹配的算法和表达式求值的算法能分开研究及学习,这本来就是可以独立出来理解的子过程。
1.2 直观印象
中缀表达式: 1 + 2
后缀表达式: 1 2 +
接下来参考王道书上3.6.11的习题解析,但采用自顶向下分析
2. 算法重要组成
2.1 数据结构
栈 (仅用一个)
操作符优先级定义表(人为规定)
2.2 行为列表
- 操作符优先级比较 (查表)
compare(栈外操作符, 栈内操作符)
值得注意的是,同样的操作符,栈内外的优先级不同, 这种查表的方式是很有意思的设计,后文将继续做分析。
操作符 | # | ( | * / | + - | ) |
---|---|---|---|---|---|
栈内优先级(被考察后入栈的元素) | 0 | 1 | 5 | 3 | 6 |
栈外优先级(当前正在考察的元素) | 0 | 6 | 4 | 2 | 1 |
- 入栈
push(栈外操作符)
- 出栈
pop(栈内操作符)
- 出栈并输出
popAndPrint(栈内操作符)
- 考察下个元素
next()
- 直接输出
print(当前正在考察的字符)
(存在不入栈即可输出的当前考察对象)
2.3 算法边界
2.3.1 初始化
算法无条件得首先执行
push(#)
即初始状态时,栈内必有一个元素。
contact(inputString, #)
待考察的字符串后面加个 #
目的是:
- 当字符串只有一个字符的时候
compare(栈外操作符, 栈内操作符)
依旧合法。 - compare(#, #) 可视为,所有字符串元素考察完成的信号
2.3.2 算法终点
当栈为空,字符串被已考察结束。程序可以结束。
3. 梳理算法执行过程
待考察字符串:a + b - a * ( (c + d) / e - f ) + g
上接 2.3 算法边界。代码块中符号[
表示栈底
step1
String: a + b - a * ( ( c + d ) / e - f ) + g #
current: ↑
# 当前考察元素为操作数为操作数,直接输出
print(a)
next()
栈 : [#
控制台 : a
step2
String: a + b - a * ( ( c + d ) / e - f ) + g #
current: ↑
# compare(#, +) 当前待考察元素(也是栈外元素)比栈顶元素优先级高,入栈
push(+)
next()
栈 : [#+
控制台 : ab
step3
String: a + b - a * ( ( c + d ) / e - f ) + g #
current: ↑
# 当前考察元素为操作数为操作数,直接输出
print(b)
next()
栈 : [#+
控制台 : ab
step4
4.1
String: a + b - a * ( ( c + d ) / e - f ) + g #
current: ↑
# compare(+, -) 当前待考察元素比栈顶元素`+`优先级低
popAndprint(+)
栈 : [#
控制台 : ab+
4.2
String: a + b - a * ( ( c + d ) / e - f ) + g #
current: ↑
# compare(#, -) 当前待考察元素比栈顶元素`#`优先级高,当前元素入栈
push(-)
next()
栈 : [#-
控制台 : ab+
step 2 和 step4 从行为上看不同,但是逻辑是统一的。描述为:任意两个操作符比较,至少输出一个优先级较大的操作符(查表),栈中保留的操作符(查表)优先级大小一定 小于等于 已经被输出的操作符。
思考两个问题:
- 栈中的元素如何保证最后一定输出完毕
算法初始化时已经人为得在字符串后拼接了 “#”,也就是栈中存放任意多的元素,都会最后一次的compare(?, #) 操作将栈中元素依次 popAndPrint(next())- 括号的数学符号意义是改变优先级,算法如何体现
2.1 任意次遇到(
: 每次都立即入栈,继续遍历
2.2 任意次遇到)
:栈依次 popAndPrint(next()),直到遇到最近的一个(
,此时出现compare('(', ')' )
,两括号之间的表达式已经被正确处理。得益于栈内'('
与栈外')'
定义为优先级相等,是一个四则运算符之间比较不具有的结果特性。具体看到step12
step5
String: a + b - a * ( ( c + d ) / e - f ) + g #
current: ↑
# 当前考察元素为操作数为操作数,直接输出
print(a)
next()
栈 : [#-
控制台 : ab+a
step6 ~ step11
根据上述描述可得
String: a + b - a * ( ( c + d ) / e - f ) + g #
current: ↑ ↑ ↑ ↑ ↑ ↑
栈 : [#-*((+
控制台 : ab+acd
step 12
String: a + b - a * ( ( c + d ) / e - f ) + g #
current: ↑
栈 : [#-*(
控制台 : ab+acd+
至此,第一组括号 ()
已处理完毕,同理,当遇到下一个 )
,效果类似,不再模拟。
整个过程结束后,即可回答前言的问题答案:同时保存在栈中的操作符的最大个数是 5 (已除去 #
)
4. 人为制定元素优先级的意义
如果元素变得可比较,Java可以用compareable接口实现,C++可用运算符重载,通用的方法是用Map<String , int> 存储,通过符号比较值大小即可。经过比较可以输出三种结果,即可赋予不同的语义。
-
当前考察元素 > 栈内元素
当前操作符入栈
现实场景:
栈内 [#+ 考察 *
=>*
入栈。
意义:
栈是后入先出,从数学角度,*
的运算确保被优先执行。而执行的时机,见2 -
当前考察元素 < 栈内元素
操作符出栈,直到当前考察元素 > 栈内元素
,当前考察元素入栈。
现实场景:
2.1栈内 [#+ 考察 -
=>+
出栈。
2.2栈内 [#+* 考察 -
=>* +
依次出栈
2.3栈内 [#+*(+ 考察 -
=>+
出栈-
入栈意义:
1)栈内永远不会出现[#++
或[#+-
或[#-+
或[#--
的情况,即从左到右遍历字符串,同等优先级的运算是从左往右依次输出。
2)优先级高的运算符如* /
从左到右离最近的+ -
先被输出。如1 + 2 * 3
,*
操作符会被先输出
3)存在(
时,此时栈内运算符(
的优先级最高,直到栈外出现)
,所有四则运算都会 “局部” 按照数学的优先级顺序进行输出。如1 + 2 * ( 3 * 1 )
,3 * 1
会被优先输出 -
当前考察元素 == 栈内元素
由2.3知道,遍历过程中出现相等的情况,即“局部”所有符号已经处理完毕,现在需要做的就是,将栈内的(
弹出即可。
现实场景:
3.1栈内 [#+*( 考察 )
=>(
出栈
3.2栈内 [# 考察 #
=>#
出栈 => 栈为空 => 程序结束