突然想起还没说怎么将实现 NFA 转 DFA,不过在那之前,我先完整解释一下什么是状态机,DFA、NFA,以及他们的工作原理等。
首先,什么是状态机,我有一个特别好的比喻(自己感觉良好O(∩_∩)O),我觉得状态机就像我小时候玩大富翁一样,我所控制的角色就是其中的一个执行状态的机器,而摇色子就是接受输入状态,摇到几,我就控制角色前进几步,然后相应的格子上指出的规则就是转换状态,这就是状态机!当然,状态机有很多现实中的例子,只是,我们平时不以为然,或者只是没想过这样的工作原理是有学术名称的。
比如:进入一些地方入口有个旋转式东西(我还真不知道这玩意名字叫什么= =,网上找到了张图):
它接受硬币或人进入则转换到对应状态,假设它有2个状态 { 开,关 },输入集有 { 接受投币,人进入 },执行过程是这样的:首先是关闭状态,关闭状态时可以接受投币,然后,跳转到打开状态,也就是旋转到放入的时候,打开状态可以接受人进入状态,然后,跳转到关闭。于是实际上就是人进入以后,又旋转到禁止进入的样子。(其实这个例子是维基百科上的。)
我们用的计算机也是状态机,只是它由很多状态机组成的(或者说图灵机),而且是有限状态的(因为内存有限的原因),所以是理论上图灵完备的(研究这方面的大佬很多,更深的一些由于我自己也很菜,并不是专业研究符号逻辑学和编译原理的,数学也贼菜,而且展开说下去的话实在太广泛了,所以不多说了,这也不影响初步对状态机的理解)。我也是对编译原理十分感兴趣,所以有略有学习。
回到主题,上面的比喻就是基本的状态机的模型实例,下面给出计算机领域中状态机的数学定义:
一个状态机用 5 元组表示为: $$ (Q, sum, Delta, q_{0}, F) $$
这里:
1. $Q$ 表示接受状态集;
2. $sum$ 表示输入符号集;
3. $Delta$ 状态转移函数,或状态转移表;
4. $q_{0}$ 初始状态;
5. $F$ 最终状态。(我个人喜欢叫做终点,就像玩大富翁抵达终点一样的终点)。
基本上叫状态机都有这5个属性,因为状态机就是靠这几条定义建立并执行的。
所以,DFA(Deterministic finite automaton 确定性有限状态机或确定性有限状态自动机)、NFA(Nondeterministic finite automaton 不确定性有限状态机或不确定性有限状态自动机)的数学定义是一样的。
那什么是 DFA, 什么是 NFA 呢。DFA 就是每个接受状态接受一个输入符号后都有确定的唯一的状态转移方式,用状态函数表示就是 $delta (q_n, a) ightarrow q_m$,而 NFA 不一样,NFA 中某些接受状态接受一个输入符号可能会跳转到两个或以上的另一个接受状态,用状态函数表示就是 $delta (q_n, a) ightarrow (q_m, q_{m + 1}, ...)$。
比如,下图是一个 DFA:
而这个是一个NFA:
注意到不同点就应该明白了,第二张图中 $q_1$ 接受一个字符 $a$,到底该往 $q_0$ 状态跳转,还是 $q_2$?,这就是不确定性,而且现实中,NFA 的情况更多。
最简单的一个 NFA 例子,考虑正则表达式 a*a,我们看下它的 NFA 构造图:
假设已经构造好了一个可以识别 a*a 表达式的状态机,设输入 'aaaaa' 这么一串字符串为输入集,开始进行匹配,显然很简单就可以匹配成功;设 'a' 为输入集,也可以匹配成功,但很显然这个正则表达式你必须要输入至少一个字符 $a$ 才能匹配正确,而它的构造却又是不确定性的,状态 $q_0$ 接受一个 $a$ 到底怎么跳转?
所以,下面就是 NFA 转 DFA 的方法了。
其实方法挺简单的,只需要按如下步骤执行即可构造出该表达式的 DFA:
设 NFA 为 $Q = {q_0, q_1, ...}, M = (Q, sum, T, q_0, F)$,DFA 为 ${Q}' = varnothing, {M}' = ({Q}', sum, {T}', null, F)$
1.${Q}' = {Q}' igcup q_0$,将 NFA 初始状态 $q_0$ 加入集合;
2.根据 NFA 构造图,写出 DFA 的状态转移表,这时 DFA 中有了新的状态,新的状态是指当某些输入字符的写出来不止一个状态都视为一个状态(有点难解释,看下面具体示例);
3.将 DFA 中新的状态加入集合;
4.重复执行第 3 步,直到没有出现新的状态时停止;
用 a*a 来具体演示一下:
它的 NFA 属性具体如下:
$Q = {q_0, q_1}$;
$sum=a$,因为这里我们处理的只有 a 这一种字符;
$T$ 状态转移表如下:
a | |
$q_0$ | $(q_0, q_1)$ |
$q_1$ |
初始状态为 $q_0$;
$F = F$,最终状态相同。
然后开始构造 DFA:
按构造公式,第一步将 NFA 的初始状态加入到 DFA 的新状态集中:
${Q}' = {q_0}$;
第二步,对照 NFA 的构造图,写出状态转移表,这里就看到 a 对应的状态新增加为 $(q_0, q_1)$,把它们整体视为一个新状态:
a | |
$q_0$ | $(q_0, q_1)$ |
第三步,将新状态加入 ${Q}'$,对应的状态表变为:
a | |
$q_0$ | $(q_0, q_1)$ |
$(q_0, q_1)$ | $(q_0, q_1)$ |
第四步,我们发现没有多的状态可以再加入了,于是上面的表就是 DFA 的状态转移表,${Q}' = {q_0, (q_0, q_1)}$ 就是它的接受状态集,$q_0$ 就是初始状态。
可以画出构造图:
你会发现变为了确定性,更清晰的匹配方式,而且,这使得我们容易写出代码。
最后,例子只是简单的一个例子,可以试着想个复杂的测试下。还有,如果感兴趣可以再去看看 kleene 算法是如何将 NFA 转为正则表达式,以及 Thompson 构造算法又是如何将正则表达式转为 NFA 的,真的很有意思。(对了,我当初用纸笔推导过 kleene 算法,结果写了两页纸的转换结果,没推出来,不知道是不是哪里姿势不对,虽然感觉这东西应该用计算机来推导,但菜鸟有点没能力写代码啊(其实没尝试过去写。。))
参考:
1. 编译原理(龙书)
2. 维基百科 - NFA
3. 维基百科 - 图灵完备性