0.前言
我總是對那些具有狀態轉移過程的算法,心懷敬意。
例如:遞歸、遞推、動規、DAT 以及現在要說的 AC 自動機算法。
數學真是優美!
—— 致那些牛逼到不行的數學家們
1.版權說明
商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
本文作者:Q-WHai
發表日期: 2015年10月24日
本文鏈接:http://blog.csdn.net/lemon_tree12138/article/details/49335051
來源:CSDN
更多內容:分類 >> 算法與數學
2.概述
Aho-Corasick automaton(後面心均以AC代替),該算法在1975年產生于貝爾實驗室,是著名的多模匹配算法之一。
AC自動機算法分爲3步:構造一棵Trie樹,構造失效指針和模式匹配過程。而這3步就是AC自動機算法的精髓所在,分別對應我們後面馬上要說的3個函數:success,failure和emits.
3.特別說明
3.0.學前導讀
在學習本文之前,需要兩個方面的知識背景。一個是Trie樹,一個是KMP算法。大家可以移步到兩面的兩個鏈接中,學習一下。之後,回過頭來再看我們的AC自動機,就可以會比較容易消化,也能更容易理解其中的精髓。
3.1.本文參考
《Aho-Corasick算法的Java實現與分析-碼農場》
4.一睹爲快
以經典的ushers爲例,模式串是he/ she/ his /hers,文本爲“ushers”。構建的自動機如圖:
5.原理說明
5.0.算法比較
正如前面所說,AC算法是基於Trie樹且是KMP模式匹配算法的擴展。那麼這裏我們就可以從兩個方面來作爲切入點,詳細說明一下AC算法或是AC自動機究竟是何物。
首先明白兩點:Trie樹的核心點是狀態轉移,KMP模式匹配的核心點是減少重複匹配。
先說Trie樹吧。在之前的博客中,我還是用了很多的篇幅來說Trie樹,不算這一篇的話,也有3篇文章或多或少都和Trie樹扯上點邊兒。前面的Trie樹中,每個節點既是字符本身,又是節點的狀態(DAT則不是這樣)。節點爲字符本身,這個好理解,那又是節點的狀態這個要怎麼解釋呢?因爲我們知道,當我們在遍歷的過程中,走到某一個點的時候,比如說:目前有兩個字典字符串:T1:"abcde"和T2:"abdef",當我們在遍歷的過程中走了"abcd"且停在了'd'字符上.這個時候,我們可以認定目前是處於字符串T1上的。因爲當前節點可以代表其狀態。而在T1和T2中,兩個'd'節點的狀態是不同的。而Trie樹的狀態轉移則可以理解爲,我們在遍歷到節點d的時候,動態確定節點d的下一個狀態,即節點e。
再說說KMP模式匹配。在KMP模式匹配的過程中,我們使用到了一個next函數(如果你高興,也可以說這是一張next表)。next函數的作用是,當我們在匹配的過程中,發生了匹配失敗的時候,可以將模式串向前滑動n個字符,從而省去了n次的比較操作。而具體的操作方法及說明,我在之前的博客中也有介紹,這裏不再詳細說明。
試想一下,如果我們要匹配一個文本文件d(舉例文件的目的是爲了說明,這個匹配字符串可能會是一個很長的字符串),使用Trie樹的匹配方式,依然需要對d進行循環遍歷,就像樸素模式匹配那樣。Trie樹減少的只是在Trie樹中重合的部分,所以時間複雜會相當高。那麼,KMP算法呢?對於KMP算法,我們要清楚一點。KMP算法是給模式串生成next函數,在多模式的情況下,我們需要生成很多的next函數,再對每個模式進行匹配。這顯然也並不理想。
基於以上這兩點,我們的AC算法誕生了。
5.1.原理
AC爲了克服Trie樹中無效匹配和KMP算法需要一個一個去匹配,設計了一種新的算法。算法中需要維護三個函數,分別是:
success:從一個狀態成功轉移到另一個狀態(有時也叫goto表或是success表)。
failure:從一個狀態按照普通流程會匹配失敗,這時我們要通過failure函數來做狀態跳轉。
emits:命中一個模式串(也稱爲output表)。
從上面的狀態轉移圖中就可以看出來,整個節點+實線就是success函數;而虛線就是failure函數;紅色節點則是emits函數。
6.代碼實現過程及說明
6.0.整體實現過程流程圖
6.1.創建Trie樹
其實AC自動機是建立在Trie的基礎之上的,從上面的狀態轉移圖中就可以獲得這一信息。而在AC算法的3個函數中的success函數就是一種Trie樹。
/**
* 構造一棵trie樹
*
* @param trieConfig
*/
public Trie(TrieConfig trieConfig) {
this(trieConfig, true);
}
public Trie(TrieConfig trieConfig, boolean ascii) {
this.trieConfig = trieConfig;
if (ascii) {
this.rootState = new AsciiState();
} else {
this.rootState = new UnicodeState();
}
}
6.2.success表的創建
從上面我們知道,success函數的功能就是構建一個棵Trie樹。關鍵是如何構建,因爲這個Trie樹的構建和我們之前說的那並不完全相同。
在AC算法中,我們把Trie樹中的節點就直接稱爲狀態(State).在創建狀態轉移表的過程中,則是利用了遞推的思想。我們在添加字典的過程中,其實是去計算當前字符對應的下一下狀態。詳細過程,請參見如下代碼:
/**
* 轉移到下一個狀態
*
* @param character 希望按此字符轉移
* @param ignoreRootState 是否忽略根節點,如果是根節點自己調用則應該是true,否則爲false
* @return 轉移結果
*/
private State nextState(Character character, boolean ignoreRootState) {
State nextState = this.success.get(character);
if (!ignoreRootState && nextState == null && this.rootState != null) {
nextState = this.rootState;
}
return nextState;
}
@Override
public State nextStateIgnoreRootState(Character character) {
return nextState(character, true);
}
@Override
public State addState(Character character) {
State nextState = nextStateIgnoreRootState(character);
if (nextState == null) {
nextState = new UnicodeState(this.depth + 1);
this.success.put(character, nextState);
}
return nextState;
}
6.3.failure表的創建
failure表的創建是一個廣度優先搜索的過程。在這個過程中,我們通過不斷遍歷狀態Trie樹。詳細編碼過程如下:
/**
* 建立failure表
*/
private void constructFailureStates() {
Queue<State> queue = new LinkedBlockingDeque<State>();
// 第一步,將深度爲1的節點的failure設爲根節點
for (State depthOneState : this.rootState.getStates()) {
depthOneState.setFailure(this.rootState);
queue.add(depthOneState);
}
this.failureStatesConstructed = true;
// 第二步,爲深度 > 1 的節點建立failure表,這是一個bfs
while (!queue.isEmpty()) {
State currentState = queue.remove();
for (Character transition : currentState.getTransitions()) {
State targetState = currentState.nextState(transition);
queue.add(targetState);
State traceFailureState = currentState.failure();
while (traceFailureState.nextState(transition) == null) {
traceFailureState = traceFailureState.failure();
}
State newFailureState = traceFailureState.nextState(transition);
targetState.setFailure(newFailureState);
targetState.addEmit(newFailureState.emit());
}
}
}
6.4.emits命中(output表的創建)
關於output表的創建,其實跟Trie樹中的結束結點標誌很類似。都是在模式串的末尾對狀態進行修改的過程。而output表則是在狀態節點對象中以組合的方式來體現。
/**
* 添加一個模式串
*
* @param keyword
*/
public void addKeyword(String keyword) {
...
currentState.addEmit(keyword);
}
/**
* 添加一個匹配到的模式串(這個狀態對應着這個模式串)
*
* @param keyword
*/
public void addEmit(String keyword) {
if (this.emits == null) {
this.emits = new TreeSet<String>();
}
this.emits.add(keyword);
}