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

news2024/7/2 20:32:38

目录

    • NFA 识别记号的并行方法
    • NFA 上识别记号的确定化方法
      • 状态集 T 的 ε-闭包(T)
      • ε-闭包算法
    • NFA 并行算法
        • NFA 并行算法例:识别 abb 和 abab
    • 从 NFA 到 DFA(子集法构造 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

识别 cc:甲 _c_ {1, 2} _c_{3, 乙},接受
识别 cbc:甲 _c_ {1, 2} _b_{3} _c_ ? ,不接受

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
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/669105.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【八大排序(八)】归并排序高阶篇-非递归版

💓博主CSDN主页:杭电码农-NEO💓   ⏩专栏分类:八大排序专栏⏪   🚚代码仓库:NEO的学习日记🚚   🌹关注我🫵带你学习排序知识   🔝🔝 归并非递归版 1. 前情回顾2. 归并非递归基…

JVM的内容

0、Java基础考点 1、谈谈你对Java的理解 平台无关性(一次编译,到处运行)GC(垃圾清理)语言特性(泛型、反射)面向对象(封装、继承、多态)类库异常处理 2、Java是如何实现平台无关性的(一处编译,到处运行) 编译时(语法和语义进行检测&#xf…

0012-TIPS-pawnyable : Use-After-Free

原文 Linux Kernel PWN | 040203 Pawnyable之UAF https://pawnyable.cafe/linux-kernel/LK01/use_after_free.html 题目下载 漏洞代码 #include <linux/module.h> #include <linux/kernel.h> #include <linux/cdev.h> #include <linux/fs.h> #includ…

【踩坑】Windows11安装WSL2,然后装miniconda

Windows11安装WSL2 跟着官方文档一步步来就完事了&#xff0c;你要在vscode上用还是用docker都有教程微软WSL文档 遇到问题 Installing, this may take a few minutes… WslRegisterDistribution failed with error: 0x80370102 Please enable the Virtual Machine Platform W…

chatgpt赋能python:Python收费怎么办?

Python收费怎么办&#xff1f; Python是一门非常流行的编程语言&#xff0c;特别是在数据科学和机器学习领域中。许多人使用Python来编写自己的应用程序和脚本&#xff0c;但是有些人会对Python的收费问题感到困惑。本文将介绍Python的收费情况以及如何解决这个问题。 Python…

Shell - 02_shell的自定义变量

一、shell的自定义变量 1.定义变量&#xff1a;变量名变量值 如&#xff1a;num10 2.引用变量&#xff1a;$变量名 如&#xff1a;i$num 把变量 num 的值付给变量 i 3.显示变量&#xff1a;使用 echo 命令可以显示单个变量取值 如&#xff1a;echo $num 4.清除变量&…

高校学生公寓安全用电物联网平台的应用

摘要:本文针对高校学生公寓用电特点,从安全用电角度提出了一套集用电管理、计量、恶性负载智能识别控制、实时跟踪检测等功能于一体的数字化安全用电管理系统技术解决方案———学生公寓智能控电管理系统。 关键词:公寓恶性负载安全用电智能系统 0、引言 近年来,为了响应国家…

webpack编译打包

1.安装webpack npm install webpack webpack-cli --save-dev 2.添加命令 在package.json文件中添加启动命令 3.打包 webpack.config.js文件 通过配置文件构建&#xff1a;npx webpack --config webpack.config.js 4.文件结构 src:用于存放代码&#xff0c;一般入口为index.…

HQChart实战教程64-自定义分时图标题栏

HQChart实战教程64-自定义分时图标题栏 分时图标题栏步骤1. 替换分时图标题格式化输出函数2. 格式化输出函数说明HQChart插件源码地址完整的demo源码分时图标题栏 分时图标题栏显示的是当前十字光标所在位置的分钟信息,显示在分时图窗口顶部。一般会显示品种的名称,日期,时间…

android内部存储和外部存储

我们在开发Android应用的过程中&#xff0c;避免不了要用到数据持久化技术&#xff0c;所谓的数据持久化就是将RAM中的瞬时数据保存到ROM中&#xff0c;保证在App退出或者手机关机后数据不会丢失。我们常用的数据持久化的方式有文件存储&#xff0c;数据库存储&#xff0c;Shar…

动手实现条件随机场(下)

引言 本文基于PyTorch实现条件随机场&#xff0c;实现CRF层参考论文Neural Architectures for Named Entity Recognition中关于CRF层的描述。包含大量的图解和例子说明&#xff0c;看完一定能理解&#xff01; 论文地址&#xff1a; https://arxiv.org/pdf/1603.01360.pdf 也可…

chatgpt赋能python:Python搜索算法:如何提高你的搜索体验

Python 搜索算法&#xff1a;如何提高你的搜索体验 在当今信息爆炸的时代&#xff0c;搜索已成为许多人获取信息的主要途径。而 Python 的搜索算法&#xff0c;也在此背景下日渐受到重视。本篇文章将深入探讨 Python 搜索算法&#xff0c;介绍以及如何使用它来提高你的搜索体验…

SpringBoot整合模板引擎Thymeleaf(5)

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 概述 在本节教程中&#xff0c;我们在之前案例的基础上详细介绍利用Thymeleaf实现国际化。 项目结构 依赖文件 请在pom.xml文件中添加如下依赖。 <?xml version"…

Android 9 蓝牙协议初始化

先讲一下Application类的使用 要使用自定义的Application&#xff0c;首先就是要自己新建一个Application的子类&#xff0c;然后把它的名字写在manifest文件里面的application标签里的android:name属性就行&#xff0c;如我的Application子类名字是BaseApplication&#xff0c…

java入门3(程序流程结构)

目录 大部分和C语言一样 顺序结构 选择结构 简单if语句 ​ 多重if结构 嵌套if结构 Switch选择结构 if和Switch嵌套 循环结构 while循环 do while 语句 for循环 break和continue break 中断指令&#xff0c;结束所在层的循环 continue:中断指令 中断本轮的循环&…

chatgpt赋能python:Python收发短信:简单可靠的解决方案

Python收发短信&#xff1a;简单可靠的解决方案 如果您需要向客户发送定期提醒或通知的短信&#xff0c;则 Python 是一种简单易用的解决方案。在本文中&#xff0c;我们将介绍如何使用 Python 发送和接收短信&#xff0c;并探讨一些流行的短信 API。 什么是短信 API&#xf…

深入分析vfio-user设备实现原理 —— Client侧

文章目录 前言数据结构protocolVFIOUserHdrvfio_user_commandVFIOUserHdr flags 设备模型VFIODeviceVFIODevIOVFIOContIO VFIOPCIDeviceVFIOKernPCIDeviceVFIOUserPCIDevice proxyVFIOUserMsgVFIOProxy 流程详解消息发松流程DMA映射流程 前言 数据结构 protocol VFIO User P…

「解析」YOLOv4模型小结

Paper Yolo v4: https://arxiv.org/abs/2004.10934 Scaled-YOLOv4: Scaling Cross Stage Partial Network Source code:https://github.com/AlexeyAB/darknet Bag of Freebies(BoF) 只增加训练成本&#xff0c;但是能显著提高精度&#xff0c;并不影响推理速度&#xff1b;数据…

Kubernetes使用Istio

Kubernetes使用Istio 1、基本概念 1.1、流量方向 南北流量&#xff08;NORTH-SOURTH-TRAFFIC&#xff09;&#xff1a;客户端到服务器之间通信的流量 东西流量(EAST-WEST-TRAFFIC)&#xff1a;指的是服务器和服务器之间的流量 1.2、Service Mesh 2、安装Istio 2.1、下载 …

【编译、链接、装载九】静态链接

【编译和链接九】静态链接 一、demo二、空间与地址分配1、相似段合并 三、即虚拟地址VMA&#xff08;Virtual Memory Address&#xff09;四、重定位1、add调用2、printf调用——同add2、shared 五、重定位表六、符号解析七、c相关问题1、重复代码消除2、全局构造与析构3、C与A…