编译原理笔记5:从正规式到词法分析器(2):NFA 记号识别、确定化、并行算法、子集法构造DFA

NFA 识别记号的并行方法

之前的文章中写过的 “用一个输入字符串在一个 NFA 中逐个尝试各种路径、最终找到一条从初态到终态” 的方法被称为“NFA识别记号的串行方法”,然而这种方法效率着实不高——一条路走不通,要退回去重新走(也就是回溯),从而产生大量的无效计算。

为了解决效率问题,我们可以改变思路,实现记号的并行识别——这种方式可以防止由于反复试探产生的回溯。

具体思路是:从起点开始,用同一个输入字符同时去尝试到下一步的所有可行的路径。这样立足于当前,把所有可能的下一步全都跳完一次,再把这些结果收集起来,就可以获得一个“从当前起点开始所有可达的下一点的集合”。
此时我们虽然不知道这个集合中哪一个才是能够满足后续需求的(也就是能最终走向终态的),但至少这个集合中该有哪些元素,已经是确定的了。
接下来,再从上一步得到的集合中的所有点同时出发,每一个状态都按照上面相同的方式去尝试所有可达的下一状态,然后将所有收集到的可达状态再放入一个新的集合——这样我们就获得了从第二个状态出发的可达状态集合……如此往复,走遍所有的状态,最后的终态节点就在最后一个可达状态的集合中。

因为我们在每一步都考虑了下一步所有可能的转移,因此收集到的状态集合,就都是“确定”的了。 我们把一个个不确定下一步收集起来变成一个下一步集合,这样就实现了将不确定的下一状态确定化

NFA 上识别记号的确定化方法

NFA 的不确定性,是由于:1. 从一个状态通过同样的字符可以达到不同的下一状态;2. 允许出现 ε 状态转移。

因此,为了消除这种不确定性就需要以下两个步骤:

  1. 消除多于一个的下一状态转移: smove(S, a),S 是状态集合,a 是字母表里的一个字符(不能是 ε );
  2. 消除 ε 状态转移:使用函数 ε-闭包(T),状态集 T 的 ε 闭包
  • smove(S, a):从状态集 S 中的每个状态出发,经过标记为 a 的边,直接到达的下一状态的全体;
  • ε-闭包(T):从状态集 T 中的每个状态出发,经过若干次 ε 转移,到达的状态全体。(经过任意有限次 ε 的都算)

状态集 T 的 ε-闭包(T)

定义:状态集 T 的 ε-闭包(T) 是一个状态集,其满足:

  1. T中所有的状态属于 ε-闭包(T); (经过若干次 ε 转移嘛,当然 0 次也是算的,零次转移所能达到的当然就是所有的自身状态)
  2. 如果 t 属于 ε-闭包(T) 且 move(t, ε)=u,则 u 属于 ε-闭包(T); (比如 t 是之前的 T 的元素经过 n 次空转移到的状态,这里的 u 就是经过 n+1 次空转移到的)
  3. 除此之外没有其他状态属于 ε-闭包(T)

所有经过 ε 跳转后抵达的状态都是结果集中的一个元素。

ε-闭包算法

闭包U:是一个集合,其存储闭包计算的结果;

栈:栈中的元素,就是当前还没有考虑的状态节点(需要我们去考虑从该处沿空边出发的节点),没有考虑空边的状态都要入栈,需要考虑更多空边的时候,就从栈里面往出取节点就行了。

function ε-闭包(T) is
begin
    for T中的每个状态t    // T 是要计算闭包的集合
    loop 
        将t加入U;// 先加入所有初状态,它们也算闭包运算结果元素
        push(t);// t是新加入的,当然没有考虑过它连接的空边,入栈
    end loop;

    while 栈非空 // 考虑经所有的状态引出的空边,能到达哪些状态
    loop        // 对每一个状态,找空边所能到的所有下一状态
        pop(t); // 栈顶的拿出来,考虑从该状态出发的空边转移情况
        for 每个u=move(t, ε)  //若存在u,可以从t经过空边跳到
        loop
            if u不在U中 then //新跳到的这个 u 并没有被加入 U
                将u加入U;
                push(u);//因为是新来的,故也没考虑过它的空边
            end if;
        end loop;
    end loop;

    return U;
end ε-闭包
复制代码

例:

图中的 U 代表我们最终返回的的结果集合,其元素是在整个算法运行的过程中被逐渐添加的;Stack 是上面伪代码中提到的“栈”,用来存储运行时临时保存的待考虑状态(也就是还没被检查所有下一状态的状态)。

NFA 并行算法

输入:NFA N, x(EOF), s0(NFA的初态),F(NFA的终态集)

输出:若 N 接受 x,打印“yes”,否则"no"

方法:用下面的过程对 x 进行识别,其中 S 是一个状态的集合

与前面的 模拟DFA 相比,有如下区别:

模拟DFA 模拟NFA
开始 初态只有一个(s0) 初态是个集合(S)
下一状态转移 得到下一个单一状态 得到下一状态集合
结束 s is in F,即终态在终态集中 S∩F ≠ Ø

但模拟 DFA 与模拟 NFA 也有一个共同点,就是【算法与模式无关】:DFA 和 NFA 都是作为数据(参数)交给算法的,算法的运行与具体的自动机无关。

NFA 并行算法例:识别 abb 和 abab

所用的 NFA 如下图所示

  • 识别 abb:

    1. 计算初态集: ε-闭包( {0} ) = {0, 1, 2, 4, 7} 记作集合A

      该步骤创建初始状态集

    2. 读取到输入字符 a,计算从 A 出发经过 a 到达的状态集:ε-闭包( smove(A, a) ) = {8, 3, 6, 7, 1, 2, 4} 记作集合B,B 的详细计算过程如下,写的比较细,懂的可以直接略过。。

      因想要的是从状态集合 A 出发进行经过 a 的状态转移再求个空闭包,因此我们需要对于集合 A 中的每个状态,都进行一次 a 状态转移,再将转移后的结果放入一个新的集合,最后对这个集合整体求一次空闭包。这一步骤,我们一步步来,首先我们建立一个临时集合 T,用于存放 A 集合中经过 a 了转移,却还没有进行闭包运算的状态。

      1. 对集合 A 中的状态 0,没有从 0 开始的 a 转移,无事发生,不需要填入集合 T;
      2. 对集合 A 中的状态 1,没有从 1 开始的 a 转移,同样不需要填入集合 T;
      3. 对集合 A 中的状态 2,其经过一次 a 转移,到达状态 3,将 3 加入 T,现在 T = {3};
      4. 对集合 A 中的状态 4,没有从该状态开始的 a 转移,不填入 T 集合;
      5. 对集合 A 中的状态 7,其经过一次 a 转移,到达状态 8,将 8 加入 T,现在 T = {3, 8};

      至此, ε-闭包( smove(A, a) ) 中的 smove(A, a) 计算完成,其结果是 smove(A, a) = T = {3, 8};

      接下来,对 T 进行闭包运算:

      1. 3 经过一次空转移,得到 6;
      2. 3 向右侧进行一次空转移,得到 7;
      3. 3 向左侧进行一次空转移,得到 1,再从这个 1 出发,进行空转移,得到 2、4;
      4. 没有从 8 开始的空转移。

      至此,ε-闭包( T ),也即 ε-闭包( smove(A, a) ) 计算完成,结果是{3, 8, 6, 7, 1, 2, 4}

    3. 读取到输入字符 b,计算从 B 出发经过 b 到达的状态集:ε-闭包( smove(B, b) )={9, 5, 6, 7, 1 , 2, 4},记为集合 C(计算方法与上一步完全相同);

    4. 读取到输入字符 b,计算从 C 出发经过 b 到达的状态集:ε-闭包( smove(C, c) )={10, 5, 6, 7, 1, 2, 4},记为集合 D (计算方法与前两步完全相同);

    5. 结束。计算 D∩{10} = {10},终态集与结果集交际非空,接受。识别的路径为 AaBbCbD

    因此,我们可以确定, 初态和终态之间存在一条为 abb 的路径。

    但实际上,对abb的识别也可以认为是:

    0 ε* A aε* B bε* C bε* D,即,通过一个输入字符进行直接转移后,再经过若干次的空转移,转移到了下一下一状态集。路径上的标记是 ε*aε*bε*bε*,去掉空转移就是 abb 了,即 ε*aε*bε*bε* = abb。

  • 识别 abab

    1. 初态集: ε-闭包( {0} ) = {0, 1, 2, 4, 7} 记作 A
    2. 从 A 出发经 a 到达:ε-闭包( smove(A, a) ) = {8, 3, 6, 7, 1, 2, 4} ,记作 B
    3. 从 B 出发经 b 到达:ε-闭包( smove(B, b) ) = {5, 9, 6, 7, 1, 2, 4} ,记作 C
    4. 从 C 出发经 a 到达:ε-闭包( smove(C, a) ) = {8, 3, 6, 7, 1, 2, 4} ,等于 B
    5. 从 B 出发经 b 到达:ε-闭包( smove(B, b) ) = {5, 9, 6, 7, 1, 2, 4} ,等于 C

    识别路径为:A a B b C a B b C,由于 C ∩ {10} = Ø,所以不接受。

观察上面的两个识别过程可以发现,当我们使用同一个 NFA 去识别两个字串时,产生了大量的重复计算(两个例子的前三步是完全相同的,第二个例子中的 3、5 也进行了完全相同的转移却又重新进行了计算)。

既然会出现对于相同输入的、重复条件下的重复计算,那么我们就可以在这里偷懒了——我们可以在正式使用一个 NFA 之前,对这个 NFA 进行预先的分析和计算,把在各种状态集情况下进行的各种转移情况计算出来,存储在一张表中。这样当我们真正分析输入序列时,就可以根据当前的状态和要进行的转移去查表、得到结果了!

这就是子集法构造 DFA 的思路——子集法构造 DFA,实际上就是对 NFA 并行识别记号方法的提前计算并记录的过程!

将 NFA 上的全部路径确定化并记录下来,就能够造出与该 NFA 等价的 DFA

下面举个例子来说明 NFA 到 DFA 的转化

这个例子假设了一个人要从甲地出发到达乙地,如下图左侧部分所示。中间 1、2、3 是途中经过的地点,转移的 c 指汽车,b 指自行车,我们要找出从甲到乙的交通方式的组合。

这个问题的模型实际上是个 NFA,就像图上画的那样。对于该 NFA,我们可以通过预计算的方式,建立一个经过状态转移到达到达状态集的 DFA(DFA 中的每个状态都是一个状态集——以原来的 NFA 中的某些状态为元素组成的集合)。该 DFA 与原 NFA 等效,能够识别 cc、ccb、cbb

DFA的优点:

  1. 消除了不确定性(将 NFA 的下一状态集合,合并为一个状态)
  2. 无需动态计算状态集合(相对于模拟 NFA 算法)

对于有 k 个状态的 NFA,与之等价的 DFA 最多有 2k 个状态(因为 DFA 中的每个状态都是 NFA 所有状态的一个子集,所以 DFA 的最大状态数量就是 NFA 的子集数量)

从 NFA 到 DFA(子集法构造 DFA )

该算法将从 NFA 的初态开始,生成可达状态机状态之间的转移关系。

输入: NFA N 输出: 等价于 N 的 DFA D。初态 ε-闭包( {s0} )(这个东西的运算结果,就是 NFA 的初态集),终态是含有 NFA 终态的状态集合。 该算法中要用到两个数据结构:Dstates(状态,用于存储生成的 DFA 状态)、Dtran(用于计算 DFA 状态之间的状态转移) 方法:用下述过程构造 DFA:

我们要将字母表中所有的字符都考虑一遍之后,才能说考虑完一个状态和与之相关的状态转移。然后再去考虑其他没有被标记的状态(也就是Dstates中的其他元素),即回到最外层的while,开始新的一轮循环——再去考虑在这个状态下,经过字母表中所有字符能够达到的状态。

最后当 Dstates 中没有剩余元素时,DFA就完全生成了。最终得到的 Dstates 和 Dtran 就是我们最终生成的 DFA (即,我们得到了一个确定的状态转移表)

例:用上述算法构造(a|b)*abb

根据这些运算的结果,我们就可以构造出来如下图所示的自动机:

嗯,这样就完成了我们的 DFA 了。

DFA 可真是个好东西,一旦有了 DFA,我们就可以根据它来简单地识别输入序列了!不用再进行那种粗野的蛮力计算了。

但,我们当前的 DFA 就已经是最优了吗?当然不,还能优化的!

再观察我们上面画出来的 DFA,不难发现(老师说不难发现,我还真就没看出来。。。),从 A 开始经过a、b能够到达的下一状态,和从 C 开始经过a、b能够到达的下一状态是相同的!(A经过a到达B、A经过b到达C;C经过a到达B、C经过b到达C)

这种情况,我们就说 A、C 是等价的:分别以这两个为初始状态,在经过不同的输入序列转移后达到的效果完全相同。

这样,我们就可以把A、C合并,改写成下面的形式——从A、C出发的都改为从0出发,修改后就能得到新的DFA,减少了一个状态。这就叫最小化 DFA

具体的最小化,下篇博客再说,这个已经太长了。。。。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容