编译原理中first和follow的计算方法

书上的定义和计算公式(以及解释)属于太长不看系列……这里试着用更简单的方式来进行表述。

概括下来,计算的时候,first看产生式的左部,follow看产生式的右部;first看第一个,follow看全部。还是用书上的例子好了:

E —> TE'
E'-> +TE'|ε
T -> FT'
T'-> *FT'|ε
F -> (E)|id

可能会看得不那么清楚。拆开来看好了:

E —> TE'
E'-> +TE'
E'-> ε
T -> FT'
T'-> *FT'
T'-> ε
F -> (E)
F -> id

计算first的时候,从左部开始,找到需要计算的目标,然后就用产生式一步一步展开,找到第一个终结符即可:

first(F)  = {(, id}
first(E') = {+, ε}
first(T') = {*, ε}
first(T)  = first(FT')
          = first(F)
          = {(, id}
first(E)  = first(TE') 
          = first(FT'E') 
          = first(F)
          = {(, id}

相比起follow,first的计算还是比较简单的。需要注意的是,first(FT’) ≠ first(F)∪first(T’),我们求的是这个整体的第一个非终结符,而不是两部分各自的第一个非终结符。

至于follow,因为follow是要找到目标非终结符后面的第一个终结符,所以需要分类讨论。比如,对于产生式A -> aBb来说,假如我们要求follow(B):

  • 如果b不为空(ε),那么就很好办了,此时follow(B) = first(b)。

    为什么?B后面的第一个不就是b的第一个吗?

  • 如果不为空(ε),但可以推导出空(ε),那么我们要跳过为空的部分,此时follow(B) = first(b) - {ε}。这个应该不用多解释,相当于跳过为空的部分。

  • 如果b为空(ε),那么相当于B后面没有东西了,此时follow(B) = follow(A)。

    为什么?这时候产生式相当于是A -> aB,B后面的第一个不就是A后面的第一个吗?

在具体计算的时候,为了避免遗漏,个人感觉可以加一些tricky的处理,加一个产生式:

E -> $E$  // here
E —> TE'
E'-> +TE'
E'-> ε
T -> FT'
T'-> *FT'
T'-> ε
F -> (E)
F -> id

添加这个产生式可以有效避免忘记把$加进follow里。计算还是比较简单的,找到右边有目标的,然后按照刚才的流程走一遍就行了。比如,对于follow(E),右边包含E的产生式有E -> $E$F -> (E),而且右边不为空,所以:

follow(E) = first($) ∪ first()) 
          = {), $}

对于follow(E’),右边包含E’的产生式有E -> TE'E'-> +TE',可以看到E’右边都为空。所以:

follow(E') = follow(E) ∪ follow(E')
          = {), $}

需要注意的一点是,出现类似于递归的情况的时候,直接去掉就好了;我们只考虑最小的情况。

对于follow(F),这个稍微复杂一点,右边包含F的产生式有T -> FT'T'-> *FT',F右边第一个是T’,既可以不为空,但是可以推导出空,又可以直接为空,所以需要取个并集:

follow(F) = follow(T) ∪ follow(T') ∪ (first(T') - {ε})
          = {+, ), $} ∪ {+, ), $} ∪ {*}
          = {*, +, ), $}

可能有一个地方还没有解释,什么叫“first看第一个,follow看全部”?因为按照定义,first是第一个符号,但follow是紧跟在目标后边的终结符的集合,并没有说只有一个。举例来说,对于产生式S -> A a A b | B b B a来说,first(A) = ε,但follow(A) = {a, b},差别就在这了。这也是我觉得非常神秘的一个地方。

发布了110 篇原创文章 · 获赞 132 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/HermitSun/article/details/103541524