Aho-Corasick算法簡稱AC算法,經過將模式串預處理爲肯定有限狀態自動機,掃描文本一遍就能結束。其複雜度爲O(n),即與模式串的數量和長度無關。html
自動機按照文本字符順序,接受字符,併發生狀態轉移。這些狀態緩存了「按照字符轉移成功(但不是模式串的結尾)」、「按照字符轉移成功(是模式串的結尾)」、「按照字符轉移失敗」三種狀況下的跳轉與輸出狀況,於是下降了複雜度。java
AC算法中有三個核心函數,分別是:git
success; 成功轉移到另外一個狀態(也稱goto表或success表)github
failure; 不可順着字符串跳轉的話,則跳轉到一個特定的節點(也稱failure表),從根節點到這個特定的節點的路徑剛好是失敗前的文本的一部分。算法
emits; 命中一個模式串(也稱output表)數組
以經典的ushers爲例,模式串是he/ she/ his /hers,文本爲「ushers」。構建的自動機如圖:緩存
其實上圖省略了到根節點的fail邊,完整的自動機以下圖:數據結構
自動機從根節點0出發併發
首先嚐試按success錶轉移(圖中實線)。按照文本的指示轉移,也就是接收一個u。此時success表中並無相應路線,轉移失敗。函數
失敗了則按照failure表回去(圖中虛線)。按照文本指示,此次接收一個s,轉移到狀態3。
成功了繼續按success錶轉移,直到失敗跳轉步驟2,或者遇到output表中標明的「可輸出狀態」(圖中紅色狀態)。此時輸出匹配到的模式串,而後將此狀態視做普通的狀態繼續轉移。
算法高效之處在於,當自動機接受了「ushe」以後,再接受一個r會致使沒法按照success錶轉移,此時自動機會聰明地按照failure錶轉移到2號狀態,並通過幾迴轉移後輸出「hers」。來到2號狀態的路不止一條,從根節點一路往下,「h→e」也能夠到達。而這個「he」剛好是「ushe」的結尾,狀態機就彷彿是壓根就沒失敗過(沒有接受r),也沒有接受過中間的字符「us」,直接就從初始狀態按照「he」的路徑走過來同樣(到達同一節點,狀態徹底相同)。
看來這三個表很厲害,不過,它們是怎麼計算出來的呢?
很簡單,瞭解一點trie樹知識的話就能一眼看穿,goto表就是一棵trie樹。把上圖的虛線去掉,實線部分就是一棵trie樹了。
output表也很簡單,與trie樹裏面表明這個節點是不是單詞結尾的結構很像。不過trie樹只有葉節點纔有「output」,而且一個葉節點只有一個output。下圖卻違背了這兩點,這是爲何呢?其實下圖的output會在創建failure表的時候進行一次拓充。
以上兩個表經過一個dfs就能夠構造出來。關於trie樹的更詳細內容,請參考:《Ansj分詞雙數組Trie樹實現與arrays.dic詞典格式》,《Trie樹分詞》,《雙數組Trie樹(DoubleArrayTrie)Java實現》。
這個表是trie樹沒有的,加了這個表,AC自動機就看起來不像一棵樹,而像一個圖了。failure表是狀態與狀態的一對一關係,別看圖中虛線亂糟糟的,不過你仔細看看,就會發現節點只會發出一條虛線,它們嚴格一對一。
這個表的構造方法是:
首先規定與狀態0距離爲1(即深度爲1)的全部狀態的fail值都爲0。
而後設當前狀態是S1,求fail(S1)。咱們知道,S1的前一狀態一定是惟一的(剛纔說的一對一),設S1的前一狀態是S2,S2轉換到S1的條件爲接受字符C,測試S3 = goto(fail(S2), C)。
若是成功,則fail(S1) = goto(fail(S2), C) = S3。
若是不成功,繼續測試S4 = goto(fail(S3), C)是否成功,如此重複,直到轉換到某個有效的狀態Sn,令fail(S1) = Sn。
原理誰均可以說幾句的,但是優雅健壯的代碼卻不是那麼容易寫的。我考察了Git上幾個AC算法的實現,發現robert-bor的實現很是好。一趟代碼看下來,學到了很多設計上的知識。我fork了下來,針對Ascii作了優化,添加了中文註釋。
另外,我實現了基於雙數組Trie樹的AC自動機:《Aho Corasick自動機結合DoubleArrayTrie極速多模式匹配》。性能更高,內存可控。
開源在https://github.com/hankcs/aho-corasick。
輸出:
此外,還有一些配置選項:
這裏封裝了Trie樹,其中比較重要的類是Trie樹的節點State:
我重構了State,將其異化爲UnicodeState和AsciiState類。其中UnicodeState類使用 Map<Character, State> 來儲存goto表,而AsciiState類使用數組 State[] success = new State[256]來儲存,這樣在Ascii表上面,AsciiState的匹配要稍微快一些,相應的在構建時會慢一些,內存佔用也會多一些。
從對萬字的英語詞典的測試結果來看,AsciiState的確有那麼一點優點:
這裏封裝了一棵線段樹,關於線段樹的介紹請查看:線段樹。
線段樹用於修飾最後的匹配結果,匹配結果中有一些可能會重疊,好比she和he,這棵線段樹對匹配結果(一系列區間)進行索引,可以在log(n)時間內判斷一個區間與另外一個是否重疊。詳細的實現請看代碼,都有中文註釋,應該很好懂。
AC自動機能高速完成多模式匹配,然而具體實現聰明與否決定最終性能高低。大部分實現都是一個Map<Character, State>了事,不管是TreeMap的對數複雜度,仍是HashMap的鉅額空間複雜度與哈希函數的性能消耗,都會下降總體性能。
雙數組Trie樹能高速O(n)完成單串匹配,而且內存消耗可控,然而軟肋在於多模式匹配,若是要匹配多個模式串,必須先實現前綴查詢,而後頻繁截取文本後綴纔可多匹配,這樣一份文本要回退掃描多遍,性能極低。
若是能用雙數組Trie樹表達AC自動機,就能集合二者的優勢,獲得一種近乎完美的數據結構。具體實現請參考《Aho Corasick自動機結合DoubleArrayTrie極速多模式匹配》。
部分圖片和介紹來自:
http://www.cnblogs.com/zzqcn/p/3525636.html
http://blog.csdn.net/sealyao/article/details/4560427