4.4.2 不同形式表达式的转换方法
一个表达式通常由操作数(Operand)、运算符(Operator)和界限符(Delimiter)三个基本要素构成:
- 操作数:可以是常数,也可以是变量或常量的标识符;
- 运算符:包括算术运算符(如
+
、-
、*
、/
)、关系运算符(如>
、<
、==
)和逻辑运算符(如&&
、||
); - 界限符:常用的包括左右圆括号
( )
、表达式结束符(如分号;
)等。
为方便描述,通常将运算符和界限符统称为算符。
为简化讨论,本节假设表达式为由 +
、-
、*
、/
四种运算符、正整数以及圆括号构成的合法算术表达式。这类表达式通常以字符串形式输入,例如:exp = "1 + 2 * (4 - 3)"
。
根据运算符与操作数的相对位置,表达式可分为以下三种常见形式:
-
中缀表达式(Infix Expression):运算符位于两个操作数之间,如
1 + 2 * 3
。这是人类最习惯使用的表达式形式,但其运算顺序需遵循以下规则:- 先乘除,后加减;
- 从左到右计算;
- 先括号内,后括号外。
中缀表达式不仅依赖运算符优先级,还需处理括号,因此直接用于计算机求值较为复杂。
-
后缀表达式(Postfix Expression):也称为逆波兰表示法(Reverse Polish Notation, RPN),运算符位于操作数之后。例如,
1 + 2 * 3
对应的后缀表达式为1 2 3 * +
。这种表达式由波兰数学家扬·卢卡西维茨(Jan Łukasiewicz)于 1920 年提出。后缀表达式无需括号,运算符顺序即表示运算顺序,特别适合基于栈结构的计算机求值,可有效减少内存访问和提高计算效率。 -
前缀表达式(Prefix Expression):也称为波兰表示法(Polish Notation),运算符位于操作数之前。例如,
1 + 2 * 3
的前缀表达式为+ 1 * 2 3
。这种表达式由波兰数学家扬·武卡谢维奇(Jan Łukasiewicz)在 1920 年代引入,最初用于简化命题逻辑。早期应用于 LISP 等函数式编程语言,其无括号的特性便于编译器解析和处理。
计算机计算表达式的值时,通常先将中缀表达式转换为后缀或前缀形式,再利用栈结构进行求值,从而避免复杂的中缀优先级和括号处理。当然,实际表达式中的算法比本节讨论的要复杂得多,比如可能包含取负(-a
)、函数调用(如 max(a, b)
)等更复杂的结构,但本节不对此进行探讨,以下内容中仅限于 +
、-
、*
、/
四种运算符、正整数以及圆括号构成的合法算术表达式。
1. 中缀表达式转换成后缀表达式
尽管后缀表达式不够直观易读,但它不需要括号,只有操作数和运算符,并且越放在前面的运算符越优先执行,也就是考虑了运算符的优先级。具体而言,各运算符被执行的次序,与其在后缀表达式中出现的次序完全吻合。下面介绍两种转换方法。
(1) 手工转换
例 4.4.1 将中缀表达式 5 + (1 + 2) * 4 - 3
转换为后缀表达式。
【解】
-
对中缀表达式,按照计算的优先级,增添足够多的括号,用以显示地指定表达式的运算次序:
((5 + ((1 + 2) * 4)) - 3)
-
按照“从里到外”的顺序,将每层括号的运算符移到与之紧邻的对应的右括号的右侧:
((5 + ((1 2) + * 4)) - 3)
(此步完成了(1 2)+
的操作,之后将(1 2)+
视为一个整体,其中的+
不再参与之后的“移动”)((5 + ((1 2) + 4) *) - 3)
((5 ((1 2) + 4) *) + - 3)
((5 ((1 2) + 4) *) + 3) -
-
去掉左右的括号:
5 1 2 + 4 * + 3 -
,最终得到后缀表达式。
可见,后缀表达式和中缀表达式相比,操作数之间的相对次序,在转换前后保持不变;而运算符在后缀表达式中所处的位置,恰好就是其对应的操作数均已就绪且该运算可以执行的位置。以上述得到的后缀表达式为例,如果用手工方式计算:
- 计算
1 2 +
,即1 + 2 = 3
,于是得到5 3 4 * + 3 -
- 计算
3 4 *
,得到5 12 + 3 -
- 计算
5 12 +
,得到17 3 -
- 最后得到结果:14
(2)利用栈转换
利用栈将中缀表达式转换为后缀表达式的基本流程是:
- 对中缀表达式从左到右扫描,将遇到的操作数直接存放到后缀表达式中。
- 遇到运算符或者左括号都暂时保存到运算符栈,而且先执行的运算符先出栈。
- 运算符入栈时需比较优先级:若当前运算符优先级不高于(小于等于)栈顶运算符,则弹出栈顶运算符至输出,直至满足入栈条件。
- 遇到右括号时,弹出栈中运算符直至左括号(左括号弹出但不输出)。
- 表达式扫描完成后,将栈中剩余运算符依次弹出并输出。
例 4.4.2 中缀表达式 exp = '1 + 2 + 3'
,利用栈转换为后缀表达式。
【解】
-
将操作数
1
存入后缀表达式序列postexp
,即postexp = '1'
; -
遇到第一个
+
,尚未确定它是否最先执行,将其进栈。至此得到图 4.4.13 所示结果:
-
将操作数
2
存入到postexp
,即postexp = 1 2
; -
遇到第二个
+
,比较两个+
的优先级,如果第二个+
进栈,则以后它一定要先出栈,表示第二个+
比第一个+
先执行,显然这是错误的,按照执行顺序,应该先执行第第一个+
。所以,此时应该先将栈中的第一个+
出栈,并存入postexp
,即得到:postexp = 1 2 +
;然后将第二个+
入栈(表示第一个+
先执行)。此时得到图 4.4.4 所示结果:
-
将操作数
3
存入到postexp
,即postexp = 1 2 + 3
; -
对
exp
扫描完毕,将运算符栈中的第二个+
出栈,并存入postexp
,得到:postexp = 1 2 + 3 +
,即为后缀表达式。
**归纳 1:**在扫描 exp
遇到一个运算符 op
时:
-
如果栈为空,直接将其进栈;
-
如果栈不空:
-
只有当
op
的优先级高于栈顶运算符的优先级时才直接将op
进栈(以后op
先出栈表示先执行它); -
否则依次出栈运算符并存入
postexp
(出栈的运算符都比op
先执行)。直到
op
的优先级高于栈顶运算符为止,即将op
进栈。
-
例 4.4.3 中缀表达式 exp = '2 * (1 + 3) - 4'
,利用栈转换为后缀表达式。
【解】
-
将操作数
2
存入后缀表达式序列postexp
,即:postexp = '2'
; -
遇到
*
,将其进栈; -
遇到
(
,将其进栈; -
将操作数
1
存入postexp
,即postexp = 2 1
; -
遇到
+
,将其进栈; -
将操作数
3
存入postexp
,即postexp = 2 1 3
; -
遇到
)
,出栈+
,并存入postexp
(postexp = 2 1 3 +
)出栈(
; -
遇到
-
,出栈*
,并存入postexp
(postexp = 2 1 3 + *
),将-
进栈; -
将操作数
4
存入postexp
(postexp = 2 1 3 + * 4
); -
此时
exp
扫描完毕,出栈-
,并存入postexp
。得到的最后结果postexp = 2 1 3 + * 4 -
。
**归纳 2:**在扫描 exp
遇到一个运算符 op
时,
- 如果
op
为(
,表示一个子表达式的开始,直接将其进栈; - 如果
op
为)
,表示一个子表达式的结束,需要出栈运算符并存入postexp
,直到栈顶为(
,再将(
出栈(但不存入postexp
); - 如果
op
是其他运算符,而栈顶为(
,直接将其进栈(但不存入postexp
)。
综上,可以将中缀表达式 exp
转换成后缀表达式 postexp
的过程写成如下流程:
//设 Optr 是运算符栈,初始为空
while(从 exp 读取字符 ch, ch!='\0'){
ch 为数字: 将后续的所有数字均依次存放到 postexp 中;
ch 为左括号 "(": 将此括号进栈到 Optr 中;
ch 为右括号 ")": 将 Optr 中出栈时遇到的第一个左括号 "(" 以前的运算符依次出栈并存放到 postexp 中, 然后将左括号 "(" 出栈;
ch 为其他运算符:
if(栈空或者栈顶运算符为 '(') 直接将 ch 进栈;
else if (ch 的优先级高于栈顶运算符的优先级)
直接将 ch 进栈;
else
依次出栈并存入到 postexp 中, 直到 ch 的优先级高于栈顶运算符, 然后将 ch 进栈;
}
若 exp 扫描完毕, 则将 Optr 中的所有运算符依次出栈并存放到 postexp 中。
例 4.4.4 将表达式 (56 - 20) / (4 + 2)
转换为后缀表达式,仅要求写出转换过程。
【解】
转换过程如下表所示:
操作 | postexp | Optr 栈(栈底 → 栈顶) |
---|---|---|
ch 为 '(' ,将此括号进栈 | ( | |
ch 为数字,将 56 存入 postexp 中 | 56 | ( |
ch 为 ‘-’ ,直接将 ch 进栈 | 56 | ( - |
ch 为数字,将 20 存入 postexp 中 | 56 20 | ( - |
ch 为 ) ,将栈中 ( 之前的运算符 - 出栈并存入 postexp 中,然后将 ( 出栈 | 56 20 - | |
ch 为 / ,将 ch 进栈 | 56 20 - | / |
ch 为 '(' ,将此括号进栈 | 56 20 - | / ( |
ch 为数字,将 4 存入 postexp 中 | 56 20 - 4 | / ( |
ch 为 + ,由于栈顶运算符为 ( ,则直接将 ch 进栈 | 56 20 - 4 | / ( + |
ch 为数字,将 2 存入 postexp 中 | 56 20 - 4 2 | / ( + |
ch 为 ) ,将栈中 ( 之前的运算符 + 出栈并存入 postexp 中,然后将 ( 出栈 | 56 20 - 4 2 + | / |
str 扫描完毕,则将 Optr 栈中的所有运算符依次出栈并存入 postexp 中,得到最终的后缀表达式 | 56 20 - 4 2 + / |