NFA構造及NFA轉化為DFA
在前一篇文章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基礎上增加了一列。由於之前在前一篇文章中已經詳細地講述過了,
這裡就不再贅述了。
下圖給出了一個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 著