深入理解Aho-Corasick自动机算法
0.前言
? 我總是對那些具有狀態轉移過程的算法,心懷敬意。
? 例如:遞歸、遞推、動規、DAT 以及現在要說的 AC 自動機算法。
? 數學真是優美!
? ? ? ? ? ??? ? ? ? ? ??? ? ? ? ? ??? ? ?—— 致那些牛逼到不行的數學家們
?
1.版權說明
商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
本文作者:Q-WHai
發表日期: 2015年10月24日
本文鏈接:https://qwhai.blog.csdn.net/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自動機,就可以會比較容易消化,也能更容易理解其中的精髓。
? 《數據結構:字典樹的基本使用》
? 《算法:模式匹配之KMP算法》
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;}@Overridepublic State nextStateIgnoreRootState(Character character) {return nextState(character, true);}@Overridepublic 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表,這是一個bfswhile (!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);}?
7.GitHub 源碼
https://github.com/qwhai/jac-core
?
8.征集
如果你也需要使用ProcessOn這款在線繪圖工具,可以使用如下邀請鏈接進行注冊:
https://www.processon.com/i/56205c2ee4b0f6ed10838a6d
總結
以上是生活随笔為你收集整理的深入理解Aho-Corasick自动机算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: FFmpeg常用命令合集
- 下一篇: 《数据结构》实验一