在前一篇文章DFA算法的实现与最小化中介绍了DFA,这篇文章将介绍NFA。
1. NFA与DFA的区别
NFA与DFA的主要区别如下:
1) 对于一个特定的符号输入,DFA只会跳转到一个状态;而NFA则可能跳转到多个状态。
2) NFA中一个状态可以不经过任何符号就可以实现状态转换(即存在ε-转移)
上面两个区别就导致了NFA匹配符号串时经常要回溯,而DFA由于状态转移时不存在不确定性,效率比DFA
高很多,但另一方面NFA比DFA更灵活。NFA与DFA之间可以相互转换,后面将介绍NFA转换为DFA的算法。
2. NFA的构造
正如在前一篇文章DFA算法的实现与最小化中一样,NFA也继承了一个抽象类FA,如下所示:
public abstract class FA {
protected List<FAState> acceptingStates; //可接收状态(结束状态)
protected List<FAState> states; //所有状态
protected List<String> alphabet; //符号字母表
//状态转移矩阵(使用邻接链表表示)
protected List<List<TransitMatElement>> stateTransitionMat;
//....
}
下面是NFA类的定义
public class NFA extends FA {
//开始状态
protected List<FAState> startStates;
//.......
}
之前定义DFA时,开始状态是用一个FAState类型的变量定义的,而这里,是用List<FAState>类型定义的。
这是因为DFA只能有一个开始状态,而NFA可以有多个开始状态。
构造NFA时,也是需要传入一个特定格式的文本文件的路径作为参数。
只不过由于NFA中可以存在ε-转移,需要在DFA的状态转移矩阵中添加一列,表示一个状态ε-转移的情况。
于是我就在DFA状态转移矩阵的基础上在最后一列的后面加上了一列,这反映在用于构造NFA的文本文件上
就是在DFA基础上增加了一列。由于之前在前一篇文章DFA算法的实现与最小化中已经详细地讲述过了,
这里就不再赘述了。
下图给出了一个NFA的例子:
这个例子与在介绍DFA时列出的例子等价,只不过这里状态3遇到a时有两种状态转换方式,
一种是转向状态4,另一种是转向自己。
下面举例说明另一种类型的NFA,这种NFA是由没有符号的弧(即ε-转移)引起的。
对于 这个 ε-转移,我们可以这样理解: 如果达到了状态4,可以不看当前的输入符号就转移到状态3。
所以,这是另外一种类型的非确定性。
3.NFA识别符号串
前面介绍过,DFA可以用来识别符号串,同样,使用NFA也可以。只不过由于NFA的不确定性,
NFA识别符号串的过程中可能会出现回溯。这样,我们就不得不将NFA识别符号串的过程中达到某一个
状态后可能跳转到的所有状态都保存起来。于是,我们就选择用栈来存放这些状态。
网上NFA识别符号串的例子很多,这里就不再举例子了,直接给出NFA识别符号串的核心算法。
/**
* 使用自动机识别符号串(深度优先遍历)
* @param words 待匹配符号串
* @return 如果接受,则返回true,否则,返回false
*/
public boolean recognize(String[] words) {
//对于每一个开始状态,逐一尝试,看能否识别输入的符号串
for(FAState state: this.startStates) {
FAState currentState = state;
int countOfWordsRecognized = 0;
// 用于存储识别的每一步中可能跳转到的所有状态
Stack<FAState> agenda = new Stack<FAState>();
while(countOfWordsRecognized <= words.length) {
if(isAccepted(currentState, countOfWordsRecognized, words.length)) {
return true;
} else if(wordsTerminatedButNotAccepted(currentState, words.length,
countOfWordsRecognized)) {
//当前开始状态下不能识别,尝试下一个开始状态
break;
} else {
int indexOfAlpha =
this.alphabet.indexOf(words[countOfWordsRecognized]);
//当前符号串不在符号字母表中,识别失败
if(indexOfAlpha < 0) {
return false;
} else {
boolean isWordsRecgnized =
generateNewStates(currentState, indexOfAlpha, agenda);
if(isWordsRecgnized) {
countOfWordsRecognized++;
}
}
}
/*选当前开始状态时,当前步所有可能的状态都已经尝试,但未能匹配当前符号串。
* 尝试下一个开始状态 */
if(agenda.isEmpty()) {
break;
} else {
currentState = agenda.pop();//进入下一个状态
}
}
}
return false;
}
其中函数generateNewStates是用来产生遇到当前符号时可能跳转到的状态,并压入栈中的。其核心代码
如下:
/**
* 添加指定的状态遇到对应的符号串时所用可能进入的状态列表到状态栈agend
* @param state
* @param indexOfAlpha
* @param agend 存放状态的栈
* @return 当前单词是否被识别
*/
private boolean generateNewStates(FAState state,
int indexOfAlpha, Stack<FAState> agend) {
int indexOfState = this.states.indexOf(state);
//获取下标为 indexOfState状态在状态转移矩阵中所对应的行
List<TransitMatElement> transitMatEleRow =
this.stateTransitionMat.get(indexOfState);
List<FAState> states = new ArrayList<FAState>();
boolean isWordRecognized = false;
for(TransitMatElement transEle: transitMatEleRow) {
//按照遇到的符号串的下标查找对应的要转移到的状态
if(transEle.getAlphaIndex() == indexOfAlpha) {
states.add(this.states.get(transEle.getStateIndex()));
isWordRecognized = true; //当前单词被识别
} else if(transEle.getAlphaIndex() == -1) { //ε-转移
states.add(this.states.get(transEle.getStateIndex()));
}
}
for(FAState curState : states) {
if(!agend.contains(curState)) { //当栈中不含有该状态时,才压入栈中
agend.add(curState);
}
}
return isWordRecognized;
}
4. NFA转化为DFA
NFA转化为DFA的一种常用方法是子集法。我是参照《编译原理及实践教程》来实现的。这里,
引用该书中内容来加以阐述。
直接看这些概念应该会很无聊,下面,引用该书中的一个例子,来加以阐述。
相信看了这些概念和例子之后,你就能够实现NFA转化为DFA的算法了。如果还觉得有问题的话,可以
参考我实现的代码,可以到这里下载(注:这里的代码与之前的文章《DFA算法的实现与最小化》中的代码是
一样的,如果你已经下载了,就不用再下载了)
5.参考资料
1. 《编译原理及实践教程》, 黄贤英,王柯柯 编著
2. 《自然语言处理综述》, [美 ] Daniel Jurafsky 著