给定一个语法,如何在C中计算FIRST和FOLLOW时避免堆栈溢出问题。当我不得不通过长时间的生产时,我的代码出现了问题。
示例:
S->ABCD
A->aBc | epsilon
B->Bc
C->a | epsilon
D->B
这只是一个语法。递归就是这样:
S->A
C->A
A->B
B->D
D->aBc | epsilon
FIRST(S)=FIRST(A)=FIRST(B)=FIRST(D)={a,epsilon}.
提供一个C(而不是C ++)代码,用于计算和打印上面语法的FIRST和FOLLOW集合,同时记住您可能会遇到一个具有多个隐含的特定非终端的第一个/后续集合的语法。
例如:
FIRST(A)=FIRST(B)=FIRST(B)=FIRST(C)=FIRST(D)=FIRST(E)=FIRST(F)=FIRST(G)=FIRST(H)=FIRST(I)=FIRST(J)=FIRST(K)={k,l,epsilon}.
这是:要获得
FIRST(A)
,您必须计算FIRST(B)
,依此类推,直到FIRST(K)
的{{1}}有终端FIRST(K)
为止},'k'
和'l'
。暗示越长,越有可能因多次递归而遇到堆栈溢出 如何在C语言中避免这种情况,但仍能获得正确的输出? 用C(不是C ++)代码解释。
epsilon
我的代码进入堆栈溢出。我怎么能避免它?
答案 0 :(得分:5)
你的程序溢出堆栈不是因为语法“太复杂”而是因为它是左递归的。由于您的程序没有检查它是否已经通过非终端递归,一旦它尝试计算first('B')
,它将进入无限递归,最终将填充调用堆栈。 (在示例语法中,不仅B
左递归,它也无用因为它没有非递归生成,这意味着它永远不能导出只包含的句子端子。)
但这不是唯一的问题。该计划至少还有两个其他缺陷:
在将终端添加到集合之前,不会检查给定终端是否已添加到非终端的 FIRST 集。因此, FIRST 集合中会有重复的终端。
程序仅检查右侧的第一个符号。但是,如果非终端可以产生ε(换句话说,非终端是可空的),则需要使用跟随符号来计算< strong> FIRST 设置。
例如,
A → B C d
B → b | ε
C → c | ε
此处, FIRST ( A )为{b, c, d}
。 (同样,关注( B )为{c, d}
。)
递归对 FIRST 和 FOLLOW 集的计算没有多大帮助。最简单的描述算法就是这个,类似于Dragon Book中提出的算法,它足以满足任何实际语法:
对于每个非终端,计算它是否可以为空。
使用上述内容,将每个非终端 N 的 FIRST ( N )初始化为前导集 N 的每个作品的符号。如果符号是右侧的第一个符号或者左侧的每个符号都可以为空,则符号是生产的前导符号。 (这些集合将包含终端和非终端;暂时不用担心。)
执行以下操作,直到循环期间未更改 FIRST 设置:
从所有 FIRST 集合中删除所有非终端。
以上假设您有一个计算可空性的算法。你也可以在龙书中找到这个算法;它有点类似。此外,你应该消除无用的制作;检测它们的算法与可空性算法非常相似。
有一种算法通常更快,实际上并不复杂得多。完成上述算法的第1步后,您已计算出 lead-with ( N , V )的关系,这是真的当且仅当非终端 N 的某些产品以终端或非终端 V 开始时,可能跳过可以为空的非终端。首先( N )是 lead-with 的传递闭包,其域仅限于终端。可以使用Floyd-Warshall算法有效地计算(无递归),或使用Tarjan算法的变体来计算图的强连通分量。 (例如,参见Esko Nuutila's transitive closure page.)