这篇博客涉及的编译原理的内容只能帮助复习,不能帮助掌握编译原理部分的知识,要深入理解还是要系统的学习。
博客最后会给出中缀转后缀的通用步骤。
非终结符用VN表示,终结符用VT表示。
优先级之间的比较用<,>,=
中缀表达式转化为后缀表达式
一、算符优先文法的介绍
1.编译原理网课推荐
- 慕课上国防科大王挺教授的编译原理。
- 教材是《程序设计语言 编译原理》(第3版),作者是陈火旺院士。
- 这篇博客的内容基于第五章的算符优先算法。
2.什么是算符文法
- 一个文法任何一个产生式右部没有连续两个挨着的非终结符(大写字母)。也就是说每两个字母之间都至少有一个非终结符(小写字母)相连,就说这是一个算符文法。
- 【正例】 E->...AaBbcD...
- 【反例】E->...AB...
3.什么是算符优先文法
教材内容。
引用一段书上P90的定义:(这里的
=,<,>
表示的是算符优先级的大小,书上=,<,>
中间还带点,键盘不会打打不出来,下面都会这样表示优先级。)
【图片】
(1)a=b,文法中有P->…ab…或P->…aQb…
- 表示…ab…同时归约为P,或者,…aQb…同时归约为P。
(2)a<b,文法中有P->…aR…,而R=>b…或R=>Qb…。(这里的R是正推导)
- 表示Qb…先归约成R,…aR…再归约为P。b先归约,所以b的优先级大于a。
(3)a>b,文法中有P->…Rb…,而R=>…a或R=>…aQ。(这里的R是正推导)
- 表示…aQ先归约成R,…Rb…再归约为P。a先归约,所以a的优先级大于b。
如果一个算符文法G中的终结符对(a,b)至多存在一种优先关系,或者不存在,那么G是一个算符优先文法。
二、算符优先分析算法的准备
1.给出一个通用的 算术表达式 文法
- 可以先看书上P90页的5.4的例子,理解一下过程。(如果手边没有书也没有关系,下面的内容跟书上的例子差不多)
- 给出文法:
文法G(E):
E-> T | E+T | E-T
T-> F | T*F | T/F
F-> (E) | i
--------------------------
[解释]:这是一个常见且用途广泛的算数表达式文法,里面有+,-,*,/,(,)和数字i。
[例子]:1 + 2 * ( (3+4) / 5 + 6) - 7 。(这里的1234567就是i)
2.三个前提
- 先把文法的每个产生式分开写
E->T
E->E+T
E->E-T
T->F
T->T*F
T->T/F
F->(E)
F->i
2.1 构建FirstVT和LastVT集合
- 介绍一下FirstVT和LastVT集合,引用书上P91的内容。
(1)
FirstVT(P)
是P的产生式右部的第一个终结符。
- [定义]
FirstVT(P)
= { a | P=>a…或P=>Qa… 。这里a是VT,Q是VN,P都是证推导,即一步或多步推导}- 针对(优先级)a < b
(2)
LastVT(P)
是P的产生式右部的最后一个终结符。
- [定义]
FirstVT(P)
= { a | P=>…a或P=>…aQ 。这里a是VT,Q是VN,P都是证推导,即一步或多步推导}- 针对(优先级)a > b
- 那么为什么要构建这两个集和?下面的2.2步就会用到。比如对产生式
E->E+T
,+
的优先级就小于
所有FirstVT(T)
的终结符。这样可以避免用观察法构造优先关系时出现的一些失误。
- 构造两个集合的方法
- 这里因用书上P91上的内容,上一张图上有,中间黑笔圈出来的部分。
(1)
FirstVT(P)
的构造:
- 若有产生式P->a…或P->Qa…,则把a添加到
FirstVT(P)
中。- 若P有产生式P->Q…,且b在
FirstVT(Q)
中,则把b也添加到FirstVT(P)
中。- [例]: 有产生式
P-> a | Q | Rc
,Q-> bkkkk
,FirstVT(P)
={a,c,b}。
(2)
LastVT(P)
的构造:(书上没有这个构造,这个对比FirstVT自己推出来的)
- 若有产生式P->…a或P->…aQ,则把a添加到
FirstVT(P)
中。- 若P有产生式P->…Q,且b在
LastVT(Q)
中,则把b也添加到LastVT(P)
中。- [例]: 有产生式
P-> a | Q | cR
,Q-> nnnb
,LastVT(P)
={a,c,b}。
- 基于上面给出的G(E)构造两个集合
FirstVT(E) = {+, -, *, /, (, i}
FirstVT(T) = {*, /, (, i}
FirstVT(F) = {(, i}
LastVT(E) = {+, -, *, /, ), i}
LastVT(T) = {*, /, ), i}
LastVT(F) = {), i}
2.2 构建非终结符之间的优先关系表
- 根据上面两个集合构建算符优先表
- 这里不考虑
#
的优先级,因为它总是低于所有的VT。
表 | + | - | * | / | ( | ) | i |
---|---|---|---|---|---|---|---|
+ | > | > | < | < | < | > | < |
- | > | > | < | < | < | > | < |
* | > | > | > | > | < | > | < |
/ | > | > | > | > | < | > | < |
( | < | < | < | < | < | = | < |
) | > | > | > | > | > | ||
i | > | > | > | > | > |
2.3 构建优先函数(简化优先关系表)
- 这个例子中的VT比较少,如果一个文法中有几百几千个VT那么维护这么一个表是很费空间的一件事。所以用优先函数来化简这张表。
- 优先函数是什么,这里引用概括书上P94的内容。
【定义】:优先函数
- 节省存储空间。
f
称为入栈优先函数,g
称为比较优先函数。通俗的说,f
表示的是左边符号的优先级大小,g
表示的是右边符号优先级的大小。
【构造方法】:
- 通过有向图构造优先函数。
- 如果a>b或者a=b就画一条fa到gb的有向边。a<b或a=b就画一条gb到fa的有向边。
- 从一个节点出发,所能到达的节点数(包括自身),赋值给函数,比如说从fa出发一共能到达3个节点,fa=3。
根据文法G(E)
:
- 通过数从一个节点出发,所能到达的节点数(包括自身),构建优先函数的关系。
下面给出从一个节点出发,所能到达节点的集合。
src{ f+ } = dst { f+, g+, f(, g), g- } = 5
src{ f- } = dst { f-, g+, f(, g), g- } = 5
src{ f* } = dst { f*, g+, f(, g), g-, g*, g/, f+, f- } = 9
src{ f/ } = dst { f/, g+, f(, g), g-, g*, g/, f+, f- } = 9
src{ f( } = dst { f(, g) } = 2
src{ f) } = dst { f), g+, f(, g), g-, g*, g/, f+, f- } = 9
src{ fi } = dst { fi, g+, f(, g), g-, g*, g/, f+, f- } = 9
=======================================================
src{ g+ } = dst { g+, f(, g) } = 3
src{ g- } = dst { g-, f(, g) } = 3
src{ g* } = dst { g*, f(, g), f+, f-, g+, g- } = 7
src{ g/ } = dst { g/, f(, g), f+, f-, g+, g- } = 7
src{ g( } = dst { g(, f(, g), f+, f-, g+, g-, f*, f/, g*, g/ } = 11
src{ g) } = dst { g), f( } = 2
src{ gi } = dst { gi, f(, g), f+, f-, g+, g-, f*, f/, g*, g/ } = 11
下面这张表可以记住,适用于很多情况。
优先函数 | + | - | * | / | ( | ) | i |
---|---|---|---|---|---|---|---|
F | 5 | 5 | 9 | 9 | 2 | 9 | 9 |
G | 3 | 3 | 7 | 7 | 11 | 2 | 11 |
- 概括的介绍一下,整个算法分析归约的流程
- 此处介绍的是书上P92的伪代码。
(1)这里先介绍一下,两头尖的结构,也就是书上说的最左素短语。这个名称时我们编译原理老师上课时说的,非常形象。
- 假设有如下的优先级关系(VT之间):
h < k < a = b = c > r
- 那么两头尖的结构就像这样:
Hh
AabBCcrR
- 在归约的过程中就把AabBCc先归约(归约最左素短语)。
(2)有了两头尖这个概念,那么整个算法的分析归约过程就变得十分清楚。整个过程就是在输入串中不断找“两头尖”,一直到所有输入串都归约到文法的开始符。
- 还是用上面(1)给出的优先关系:做下面的分析(只是举例子,不是很严谨,因为输入串中不可能有VN,这里只是为了理解该步骤)
步骤 | 符号栈 | 输入串 | 动作 |
---|---|---|---|
1 | #Hh | kQaAbBcrR# | 例子最开栈里的内容 |
2 | #Hhk | QaAbBcrR# | 栈顶 h < 当前输入符号k。k移进 |
3 | #HhkQa | AbBcrR# | 栈顶 k = 当前输入符号a。a移进。(这里Q移进是因为在步骤2、3之间发生了归约,如果不理解可以忽略,接下来的步骤6会说明) |
4 | #HhkQaAb | BcrR# | a = b。b移进 |
5 | #HhkQaAbBc | rR# | b = c。c移进 |
6 | #HhkP | rR# | 因为 c > r,出现了两头尖的结构,即k < a = b = c > r ,所以把k 到r 之间的内容根据P->QaAbBc 归约为P 。 |
- 不断循环上面表格里的步骤2到6,直到符号栈内是
#E
,输入串是#
,则扫描成功。(这里的E是文法的开始符)- 结合下面408的真题可能会理解的更深。
3.小结
小结:在算符优先算法的分析指导的归约过程中,有以下特点:
1. 每次归约都是严格按照优先函数的指导进行归约,永远是高优先级的先归约。
2. 当且仅当符号栈内出现“两头尖”的结构时进行归约。
3. 每次归约的都是最左素短语。
三、以 408【2012 统考真题】为例子套用上述方法分析
1.题目
【2012 统考真题】已知操作符包括 +、 -、 *、 /、(、 )。将中缀表达式
a+b-a*((c+d)/e-f)+g
转换为等价的后缀表达式ab+acd+e/f-*-g+
时,用栈来存放暂时还不能确定运算次序的操作符。若栈初始时为空,则转换过程中同时保存在栈中的操作符的最大个数是(A)。
- A. 5
- B. 7
- C. 8
- D. 11
2.用一个简单的观察法把中缀转换为后缀
- 给出的运算符有这样的优先级:括号>乘除>加减。那么就根据优先级从小到大的顺序对中缀表达式分组。有如下步骤:
分组:Ⅰ={a+b}, Ⅱ={a*((c+d)/e-f)}, Ⅲ={g}。
那么有,原式=Ⅰ-Ⅱ+Ⅲ。
由观察法得后缀表达式:原式(后缀)=ⅠⅡ-Ⅲ+。
同理对Ⅰ,Ⅱ,Ⅲ里面按照这种方法再分组,用观察法得出每个小组得后缀表达式,再接到一起。
Ⅰ变为后缀:ab+
Ⅱ再分组:Ⅱ①={a}, Ⅱ②={((c+d)/e-f)}, Ⅱ=Ⅱ①*Ⅱ②
Ⅱ转换成后缀:Ⅱ①Ⅱ②*
原式(后缀)=ⅠⅡ①Ⅱ②*-Ⅲ+
...
...
...
最后得,原式= ab+acd+e/f-*-g+
3.用算符优先分析算法把中缀转换为后缀
-
根据上面第6条优先算法的分析流程:
-
这里有一点需要注意,就是根据在归约的时候,只要产生式形状相近就可以归约,不用VN之间一一对应,比如:产生式
E->E-T
那么当栈内出现F-T
时,可以直接把F-T归约为E
。
- 对于该题目的详细分析步骤
分析步骤:如下,
3.1小结
- 那么问题来了,这个过程和后缀表达式有什么关系呢?
不难发现:
1. 每次归约的符号连起来正好是后缀表达式 ,即上图最右边一列方括号里的符号。
(方括号里标红的两项,是因为有括号需要把之前的运算结果归约,因为在后缀表达式里没有括号,所以这里不要带括号的项。最后结果是对的。)
2. 连起来写就是ab+acd+e/f-*-g+
,到这里,整个中缀转后缀表达式的原理就大致搞清楚了。
4.给出王道的答案(图片)
参考资料
- 《程序设计语言 编译原理》(第3版)
- 王道论坛2022年数据结构考研复习指导
- 国防科大王挺教授的慕课