Aho-Corasick算法的Java實現與分析

簡介

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出發併發

  1. 首先嚐試按success錶轉移(圖中實線)。按照文本的指示轉移,也就是接收一個u。此時success表中並無相應路線,轉移失敗。函數

  2. 失敗了則按照failure表回去(圖中虛線)。按照文本指示,此次接收一個s,轉移到狀態3。

  3. 成功了繼續按success錶轉移,直到失敗跳轉步驟2,或者遇到output表中標明的「可輸出狀態」(圖中紅色狀態)。此時輸出匹配到的模式串,而後將此狀態視做普通的狀態繼續轉移。

算法高效之處在於,當自動機接受了「ushe」以後,再接受一個r會致使沒法按照success錶轉移,此時自動機會聰明地按照failure錶轉移到2號狀態,並通過幾迴轉移後輸出「hers」。來到2號狀態的路不止一條,從根節點一路往下,「h→e」也能夠到達。而這個「he」剛好是「ushe」的結尾,狀態機就彷彿是壓根就沒失敗過(沒有接受r),也沒有接受過中間的字符「us」,直接就從初始狀態按照「he」的路徑走過來同樣(到達同一節點,狀態徹底相同)。

構造過程

看來這三個表很厲害,不過,它們是怎麼計算出來的呢?

goto表

很簡單,瞭解一點trie樹知識的話就能一眼看穿,goto表就是一棵trie樹。把上圖的虛線去掉,實線部分就是一棵trie樹了。

output表

output表也很簡單,與trie樹裏面表明這個節點是不是單詞結尾的結構很像。不過trie樹只有葉節點纔有「output」,而且一個葉節點只有一個output。下圖卻違背了這兩點,這是爲何呢?其實下圖的output會在創建failure表的時候進行一次拓充。

以上兩個表經過一個dfs就能夠構造出來。關於trie樹的更詳細內容,請參考:《Ansj分詞雙數組Trie樹實現與arrays.dic詞典格式》,《Trie樹分詞》,《雙數組Trie樹(DoubleArrayTrie)Java實現》。

failure表

這個表是trie樹沒有的,加了這個表,AC自動機就看起來不像一棵樹,而像一個圖了。failure表是狀態與狀態的一對一關係,別看圖中虛線亂糟糟的,不過你仔細看看,就會發現節點只會發出一條虛線,它們嚴格一對一。

這個表的構造方法是:

  1. 首先規定與狀態0距離爲1(即深度爲1)的全部狀態的fail值都爲0。

  2. 而後設當前狀態是S1,求fail(S1)。咱們知道,S1的前一狀態一定是惟一的(剛纔說的一對一),設S1的前一狀態是S2,S2轉換到S1的條件爲接受字符C,測試S3 = goto(fail(S2), C)。

  3. 若是成功,則fail(S1) = goto(fail(S2), C) = S3。

  4. 若是不成功,繼續測試S4 = goto(fail(S3), C)是否成功,如此重複,直到轉換到某個有效的狀態Sn,令fail(S1) = Sn。

Java實現

原理誰均可以說幾句的,但是優雅健壯的代碼卻不是那麼容易寫的。我考察了Git上幾個AC算法的實現,發現robert-bor的實現很是好。一趟代碼看下來,學到了很多設計上的知識。我fork了下來,針對Ascii作了優化,添加了中文註釋。

另外,我實現了基於雙數組Trie樹的AC自動機:《Aho Corasick自動機結合DoubleArrayTrie極速多模式匹配》。性能更高,內存可控。

開源項目

開源在https://github.com/hankcs/aho-corasick

調用方法

 
  1.         Trie trie = new Trie();
  2.         trie.addKeyword("hers");
  3.         trie.addKeyword("his");
  4.         trie.addKeyword("she");
  5.         trie.addKeyword("he");
  6.         Collection<Emit> emits = trie.parseText("ushers");
  7.         System.out.println(emits);

輸出:

 
  1. [2:3=he, 1:3=she, 2:5=hers]

此外,還有一些配置選項:

 
  1.     /**
  2.      * 大小寫敏感
  3.      * @return
  4.      */
  5.     public Trie caseInsensitive()
  6.     {
  7.         this.trieConfig.setCaseInsensitive(true);
  8.         return this;
  9.     }
  10.  
  11.     /**
  12.      * 不容許模式串在位置上先後重疊
  13.      * @return
  14.      */
  15.     public Trie removeOverlaps()
  16.     {
  17.         this.trieConfig.setAllowOverlaps(false);
  18.         return this;
  19.     }
  20.  
  21.     /**
  22.      * 只匹配完整單詞
  23.      * @return
  24.      */
  25.     public Trie onlyWholeWords()
  26.     {
  27.         this.trieConfig.setOnlyWholeWords(true);
  28.         return this;
  29.     }

org.ahocorasick.trie包

這裏封裝了Trie樹,其中比較重要的類是Trie樹的節點State:

我重構了State,將其異化爲UnicodeState和AsciiState類。其中UnicodeState類使用 Map<Character, State> 來儲存goto表,而AsciiState類使用數組 State[] success = new State[256]來儲存,這樣在Ascii表上面,AsciiState的匹配要稍微快一些,相應的在構建時會慢一些,內存佔用也會多一些。

 

 

從對萬字的英語詞典的測試結果來看,AsciiState的確有那麼一點優點:

 
  1. asciiTrie adding time:1013ms
  2. unicodeTrie adding time:96ms
  3.  
  4. asciiTrie building time:903ms
  5. unicodeTrie building time:312ms
  6.  
  7. asciiTrie parsing time:355ms
  8. unicodeTrie parsing time:463ms

org.ahocorasick.interval包

這裏封裝了一棵線段樹,關於線段樹的介紹請查看:線段樹

線段樹用於修飾最後的匹配結果,匹配結果中有一些可能會重疊,好比she和he,這棵線段樹對匹配結果(一系列區間)進行索引,可以在log(n)時間內判斷一個區間與另外一個是否重疊。詳細的實現請看代碼,都有中文註釋,應該很好懂。

基於雙數組Trie樹的Aho Corasick自動機

AC自動機能高速完成多模式匹配,然而具體實現聰明與否決定最終性能高低。大部分實現都是一個Map<Character, State>了事,不管是TreeMap的對數複雜度,仍是HashMap的鉅額空間複雜度與哈希函數的性能消耗,都會下降總體性能。

雙數組Trie樹能高速O(n)完成單串匹配,而且內存消耗可控,然而軟肋在於多模式匹配,若是要匹配多個模式串,必須先實現前綴查詢,而後頻繁截取文本後綴纔可多匹配,這樣一份文本要回退掃描多遍,性能極低。

若是能用雙數組Trie樹表達AC自動機,就能集合二者的優勢,獲得一種近乎完美的數據結構。具體實現請參考《Aho Corasick自動機結合DoubleArrayTrie極速多模式匹配》。

 

Reference

部分圖片和介紹來自:

http://www.cnblogs.com/zzqcn/p/3525636.html

http://blog.csdn.net/sealyao/article/details/4560427

相關文章
相關標籤/搜索