词法分析(5):Boss战(上)——从DFA到NFA

15 阅读13分钟

本文是本人撰写的编译原理讲义。

本系列讲义适用于:被强迫学习编译原理前端,或者希望弄明白如何做科研的人

1。序章

自上一章拿到DFA这把似乎能解决词法分析一切问题的终极武器之后,主角稍作休整,便向着自词法分析(2)便矗立在那里的大Boss“万法归一”青春版发起了冲锋。(作者注:青春版一般会比原版略有缩水,比之前的版本少了for关键词的识别。)

然而就在主角对着第一条产生式砍下第一刀之后,赫然发现,居然0伤害。

S->A | C | K

???

这什么东西?

没有实义符号的话还怎么生成对应的DFA?

DFA中每条产生式都要有实义符号作为激励的呀??这怎么玩?

反击的Boss随手丢出了一个平A:

K -> if T_KEYIF | if B_TEMP

????

这又是什么东西,K状态遇到了if之后到底是要跳到T_KEYIF状态还是B状态???

不是说好DFA的特定状态遇到特定激励只会进入到一个确定的状态吗?

DFA根本处理不了这Boss一点啊!!!

陷入混乱的主角两眼一黑,含恨结束了和Boss的首场战斗。

2。复盘没思路?暴力检查!

尽管在之前的经历中,DFA确实有两把刷子。

但从首次实战的结果来看,不得不承认,从正则文法到DFA的转换确实存在很大的鸿沟。

这到底是DFA的本质问题,就应该直接进行舍弃另起炉灶,还是说我们需要对DFA进行强化?

就像当年苏联老大哥都倒了,我们还要不要坚持姓社?

从科学的角度来讲,不加分析直接姓资姓社都不是好答案。只有深入分析问题的根源,给出详细的论证,才是真正该走的道路。

而从成本论的角度来说,除非能证明我们现在走的这条路是绝对的死路,或者可以直接给出更优的而又完全不同的方案,否则都不要轻易走一条改弦易辙的道路,而更应该走改良优化的方案

姓社姓资的问题总设计师已经给过结论了,此处不做赘述,我们还是把目光放回到我们在打Boss中遇到的问题。

我们有充分的理由相信,对于其他几条ABC正则文法,我们的DFA是完全有能力处理的。

无奈SK这II个贵是真的难缠。

虽说知道问题就出在这两身上,但DFA身上到底还有什么可以改造才能解决这两个问题呢?

于是我们拿起手中的DFA定义,仔细检查它5个成份各自的定义和限制:

1,Q :一个元素数量有限的状态集合

这里的有限特别扎眼。我们能不能放宽一下这个设定,让这个集合中的元素可以无穷多个?

随便改设定是容易的,但这么做的意义是什么?

首先,这些元素本身的存在意义,是在当输入一些符号串的时候,通过状态跳转起到简单的记录作用。

然而我们输入的符号串是有限长的。

如果字符串是无限长的,而且每增长一个符号其含义都发生改变,那说明我们将永远没法准确地识别它,就像是一个无理数那样。

你说,那无理数不是可以通过取前缀来四舍五入吗?单词也这样操作不行吗?

对的,既然我们都已经四舍五入取了一个定长前缀,那为什么我们非要设置一个无限大的状态集合呢?要知道,状态的意义就在于简单地记录输入的前缀,既然要记录的信息(前缀)有限,那么我们就没必要用无限大的状态集合。

因此,集合Q不存在改进空间。

2,Σ :一个有限的输入字母表

这个集合对应的是被识别单词的组成成分表。这个表如果改变,那么识别的内容也就变了。

我们现在并不是对识别的内容有意见,而是由于识别的方法没到家,导致识别失败。所以,Σ也不存在改进空间。

3,F:一个接收状态的集合

这个集合本身就是Q的子集,他可以包含0个、一个,甚至任意有限个元素。既然Q都没有变化,那么F也不存在改进空间。

4,q₀:起始状态

DFA中只允许一个起始状态,如果我们允许多个起始状态,那么以前由于只要用一个标记头就可以模拟状态跳转,现在需要从多个起始状态开始模拟,从单线程变成多线程了,这启发了我们,对于Boss的那招平A,似乎可以通过类似的方法进行。对于多起始状态这一个点子,我们暂且持保留态度。

5,δ:状态转移函数

这个函数的定义很简单,特定状态遇到特定符号之后会跳转到特定的,也就是一个确定的状态中。

也就是δ(S,a)=S'

其中,a是属于Σ 的元素,一个实义符号;S和S'分别是集合Q中的元素。

我们无法处理S->A | C | K是因为状态转移之前没有实义符号。“没有”意味着为空,前面提到,ε代表了空符号串,因此上式可看作是S->εA | εC | εK。

这前导符号不就出来了么?

只要我们允许激励是空串,新增状态,把箭头上的符号写作ε,那都是容易的。但我们不禁要思考一个问题:

1.如果允许激励是ε,改造的意义又是什么?

既然空串代表无,那我们想想,那么数字0对于加法有什么用?1+0不还是等于1吗?

但有了0之后,我们就有了发生加法之后但什么都不变的权利。

同理,有了ε之后,我们可以让状态发生变化,但整体上什么都不变,从而轻松处理类似S->A | C | K等没有前导符号,或者是S->ε之类的情况。

2. δ函数的结果能否是一个多值函数?

如果引入多值函数,我们就能轻易解决K -> if T_KEYIF | if B_TEMP 的问题。

我们在q₀:起始状态中发现,通过把单线程变成多线程,多路模拟的问题很容易实现。

这意味着假如我们允许从一个状态在遇到特定激励之后,它可以同时跳转到多个状态。具体而言,在K遇到if之后,将分别跳转到{B, T_KEYIF}的叠加态中。就像那只被折磨的可怜的猫,在盒子中同时处于死或生的叠加态。

那问题是,我怎么知道到底我走到了哪一个状态去呢?人家薛定谔的猫好歹还要开盒观测一下让状态坍塌,你现在我就是一边看着状态分裂,最后还是不清楚的呀?

换句话来说,一个状态在遇到特定激励之后,其后跳转的状态可能不再唯一,也即不确定。

试想一下,我们之前的定义中,如果一个状态无法处理某些符号,那么它就会跳转到Error状态。换句话来说:如果特定状态处理不了某个串,那么这个串终究不会让这个状态跳转到接受态,要么是让这个状态在变化的过程中跳入到Error的状态中,要么就是在这个串结束输入的时候,没有停在接受态上。反之,如果某个符号串是合法的,那么它在图的某个分支上总会落入到接受态中。

经过这样改造的有限状态自动机,就不再是确定的(Deterministic)有限自动状态机(Finite Automaton),而是不确定(Non-deterministic)有限自动状态机NFA。

所以,一个串输入一个NFA之后,结局无非只能是以下几种情况:

a)最后如果刚好只活下来一个接受态,那这个串被这个接受态接受;

b)如果剩下了多个状态,而里面只有一个接受态,同上;

c)如果说活下来的多个状态里有多个接受态,而这些接受态宣称识别到的单词一样,那与第一种情况一样;

d)如果多个接受态宣称识别到的单词不一样,那么说明这个单词会令机器产生歧义。对于这种情况,一方面可以在设计词法规则的时候就进行避免,另一方面可以给这几个接受态分别设置一个优先级,从而在发生冲突的时候优先选择优先级较大的那个作为生效的接受态动作。

3. 还需要多起始状态吗?

既然一个初始状态允许通过ε跳转成多个新的状态,那么原本设想的多个起始状态就没了意义。因为我们大可以通过这种方法把孤零零悬着的几个所谓初始状态通过这种方法进行连接,从而让多个起始状态恢复为一个。

4.升级后的转移函数δ要怎么形式化定义?

在DFA中,我们提到δ的形式化定义:Q×Σ→Q

由于结果是一个状态,而所有可能的状态就组成了集合Q,所以我们说Q是对应的值域。

而NFA的δ函数的值域是多个状态,那么这些状态就组成了一个集合。所有可能集合组成的集合是什么?

啊好难想。那先想想这些集合中的元素从哪儿来的?

还是Q。

那么,这些集合必然是Q的子集吗?

是的。

那么,集合Q一共有多少个不一样的子集?

由于Q的元素各不相同,里面的元素只有取或不取两种状态。假设元素个数为n,那么一共有 个不同的子集。对于Q这 个互不相同的子集组成的集合,我们把它称为Q的幂集(power set) .

接下来讨论定义域。在δ函数中,一个激励可以是一个空串ε,也可以是Σ上的任意符号。

由此,我们得到NFA的δ函数的形式化定义:

5. 在给定的功能下,NFA是唯一的吗?

参照DFA的定义,NFA的定义如下:

NFA={Q, Σ, F, q₀, δ}

由于其中有五个组件,这也称为NFA五元组。其中,

Q={S, A, C, E},

Σ={a, b, c},

F = {E},

q₀=S, ,

δ(S,a)={S',S''...}//详细映射方式略。

只要 和 中间有组件不一样,你就可以认为他们完全不一样。

比如以S->A |C |K为例,由于ε边连接状态毫无成本,所以可以随便构造出其他等价的NFA。

同理,我们还可以通过魔改Q,起始符号 ,接受态集合F等元素,从而构造出功能完全一样但定义完全不同的NFA五元组。

但是,这些看上去不一样的NFA,只要相同的串可以让他们都进入接受态,那么他们就是等价的。

6.NFA还有提升的空间吗?

现有的转移函数要么只能接受单个符号,要么是空串,导致对于K -> if T_KEYIF这样的情况,我们不得不在接受i之后插入一个中间状态。

那能不能让激励成为一个长度大于等于0的符号串?这样的处理可以让我们在处理一个长字符串的时候,减少很多无用的中间状态。

经过这个改动的转移函数,称为 函数(比δ多了一个尖尖的帽子),也称为扩展转移函数

那函数的定义域如何表示?

我们先讨论长度为1的串,容易知道这个串是Σ中的元素;

对于长度为2的串,相当于前后两个符号分别从Σ中取。借用笛卡尔积的定义,我们可以表示写作Σ×Σ。

对于长度为3的串,有Σ×Σ×Σ。那对于长度为n的串,不犯傻的你想必会义无反顾写出 .

对于n=0的情况,容易发现其意义就是指一个不取Σ中的符号,得到的就是空串ε。说明我们用一个幂函数(n≥0)就可以表示任意长度为定值n的串的集合。

但问题是,我们实际的输入是不定长的。为此我们借用一个通配符“*”(星号),用来表示不定的n,从而代表这一系列集合的并集:

这就是扩展转移函数的定义域。

通过在集合上标加星星的操作,我们得到了一种一(个集合)生万物操作,从而求出一个巨大的集合。这个集合包含了通过重复选取原始集合中的元素并连接的方式得到的一切结果。我们把包括空串的那个巨大集合称为闭包;把不包括空串的那个集合称为正闭包,类比正整数。对于在集合上标加星星/加号的操作,我们称为求该集合的闭包/正闭包。

3。小结

至此我们在Boss中遇到的两大问题,通过把DFA进行升级后得以解决。

我们遇到的两个问题,核心实际上就是正则文法中存在一些DFA无法处理的问题,包括空串跳转以及同时处于路口的叠加态。而DFA无法处理的核心原因是,它非常简单,只能用了类似单线程的方法模拟图中的跳转,而且每次跳转只能是遇到实义符号才能跳转。由此我们就对DFA进行了升级,主要是从单线程升级成多线程,并且令空串成为一种激励。

对于升级后的DFA,由于每个状态的后继变得不再是确定的唯一状态,所以我们把它命名为不确定有限自动机(Non-deterministic Finite Automaton)

手握升级后的武器,主角迫不及待又向着Boss冲过去了。

这次主角是否能打过Boss呢?我们下回再讲。