算法 09| 多模式匹配算法| AC自動機

 

   BBS等文本內容網站,大都會有敏感詞過濾功能,用來過濾掉用戶輸入的一些淫穢、反動、謾罵等內容。

實際上,這些功能最基本的原理就是字符串匹配算法,也就是通過維護一個敏感詞的字典,當用戶輸入一段文字內容之後,通過字符串匹配算法,來查找用戶輸入的這段文字,是否包含敏感詞。如果有,就用“***”把它替代掉。

單模式字符串匹配算法都可以處理這個問題。但是,對於訪問量巨大的網站來說,比如淘寶,用戶每天的評論數有幾億、甚至幾十億。這時候,對敏感詞過濾系統的性能要求就要很高。畢竟,我們也不想,用戶輸入內容之後,要等幾秒才能發送出去吧?我們也不想,爲了這個功能耗費過多的機器吧?

如何實現一個高性能的敏感詞過濾系統,即多模式串匹配算法。

1. 基於單模式串和Trie樹實現的敏感詞過濾

字符串匹配算法,有 BF 算法、RK 算法、BM 算法、KMP 算法(單模式串匹配算法),還有 Trie 樹(多模式串匹配算法)

單模式串匹配算法,是在一個模式串和一個主串之間進行匹配,也就是說,在一個主串 中查找一個模式串。

多模式串匹配算法,就是在多個模式串和一個主串之間做匹配,也就是說, 在一個主串中查找多個模式串。

        儘管,單模式串匹配算法也能完成多模式串的匹配工作。可以針對每個敏感詞,通過單模式串匹配算法(比如 KMP 算法)與用戶輸入的文字內容進行匹配。但這樣做,每個匹配過程都需要掃描一遍用戶輸入的內容。整個過程下來就要掃描很多遍用戶輸入的內容。如果敏感詞很多,比如幾千個,並且用戶輸入的內容很長,假如有上千個字符,那就需要掃描幾千遍這樣的輸入內容。很顯然,這種處理思路比較低效。

      與單模式匹配算法相比,多模式匹配算法在這個問題的處理上就很高效了。它只需要掃描一遍主 串,就能在主串中一次性查找多個模式串是否存在,從而大大提高匹配效率。Trie 樹就是一種多模式串匹配算法。

那如何用 Trie 樹實現敏感詞過濾功能呢?

      對敏感詞字典進行預處理,構建成 Trie 樹結構。這個預處理的操作只需要做一次,如果敏感詞字典動態更新了,比如刪除、添加了一個敏感詞,只需要動態更新一下 Trie 樹 就可以了。
當用戶輸入一個文本內容後,我們把用戶輸入的內容作爲主串,從第一個字符(假設是字符 C) 開始,在 Trie 樹中匹配。當匹配到 Trie 樹的葉子節點,或者中途遇到不匹配字符的時候,我們 將主串的開始匹配位置後移一位,也就是從字符 C 的下一個字符開始,重新在 Trie 樹中匹配。

基於 Trie 樹的這種處理方法,有點類似單模式串匹配的 BF 算法。單模式串匹配算法中,KMP 算法對 BF 算法進行改進,引入了 next 數組,讓匹配失敗時,儘可能將模式串往後 多滑動幾位。借鑑單模式串的優化改進方法,能否對多模式串 Trie 樹進行改進,進一步提高 Trie 樹的效率呢?這就要用到 AC 自動機算法了。

2. 經典的多模式串匹配算法:AC 自動機

AC 自動機算法,全稱 Aho-Corasick 算法。其實,Trie 樹跟 AC 自動機之間的關係,就像單串匹配中樸素的串匹配算法,跟 KMP 算法之間的關係一樣,只不過前者針對的是多模式串而已。所以,AC 自動機實際上就是在 Trie 樹之上,加了類似 KMP 的 next 數組,只不過此處的 next 數組是構建在樹上罷了。

AC 自動機的構建,包含兩個操作:

  • 將多個模式串構建成 Trie 樹;
  • 在 Trie 樹上構建失敗指針(相當於 KMP 中的失效函數 next 數組)。

構建好 Trie 樹之後,如何在它之上構建失敗指針?

比如:4 個模式串,分別是 c,bc,bcd,abcd;主串是 abcd。

 

Trie 樹中的每一個節點都有一個失敗指針,它的作用和構建過程,跟 KMP 算法中的 next 數組極其相似。要先理解 KMP 算法中 next 數組的構建過程。

假設沿Trie樹走到p節點,即下圖中的紫色節點,那p的失敗指針就是從root走到紫色節點形成的字符串abc,跟所有模式串前綴匹配的長可匹配後綴子串,就是箭頭指的 bc 模式串。

這裏的最長可匹配後綴子串: 字符串abc的後綴子串有兩個 bc,c,拿它們與其他模式串匹配,如果某個後綴子串可以匹配某個模式串的前綴,那就把這個後綴子串叫作可匹配後綴子串。

從可匹配後綴子串中,找出長的一個,就是剛剛的最長可匹配後綴子串。將p節點的失敗指針指向那個最長匹配後綴子串對應的模式串的前綴的後一個節點,就是下圖中箭頭指向的節點。

  

計算每個節點的失敗指針這個過程看起來有些複雜。其實,如果把樹中相同深度的節點放到同一層,那麼某個節點的失敗指針只有可能出現在它所在層的上一層。
可以像KMP算法那樣,當要求某個節點的失敗指針的時,我們通過已經求得的深度更小的那些節點的失敗指針來推導。也就是說,我們可以逐層依次來求解每個節點的失敗指針。所以,失敗指針的構建過程,是一個按層遍歷樹的過程。
首先 root的失敗指針爲NULL,也就是指向自己。當我們已經求得某個節點 p 的失敗指針之後,如何尋找它的子節點的失敗指針呢?
我們假設節點p 的失敗指針指向節點q,我們看節點 p 的子節點 pc 對應的字符,是否也可以在節點q 的子節點中找到。如果找到了節點q 的一個子節點 qc,對應的字符跟節點 pc 對應的字符相同,則將節點 pc 的失敗指針指向節點 qc。

 

如果節點q 中沒有子節點的字符等於節點 pc 包含的字符,則令 q=q->fail(fail 表示失敗指 針,這裏有沒有很像 KMP 算法裏求 next 的過程?),繼續上面的查找,直到 q 是 root 爲 止,如果還沒有找到相同字符的子節點,就讓節點 pc 的失敗指針指向 root。

 

按層來計算每個節點的子節點的失效指針,剛剛那個例子,最後構建完成之後的 AC 自動機就是下面這個樣子:
  

AC 自動機構建完成。現在來看下,如何在 AC 自動機上匹配主串?
還是拿之前的例子來講解。在匹配過程中,主串從 i=0 開始,AC 自動機從指針 p=root 開始,假設模式串是 b,主串是 a。

  • 如果 p 指向的節點有一個等於 b[i] 的子節點 x,我們就更新 p 指向 x,這個時候我們需要通過失敗指針,檢測一系列失敗指針爲結尾的路徑是否是模式串。這一句不好理解,你可以結合代碼看。處理完之後,我們將 i 加一,繼續這兩個過程;
  • 如果 p 指向的節點沒有等於 b[i] 的子節點,那失敗指針就派上用場了,我們讓 p=p->fail,然後繼續這 2 個過程。

AC 自動機實現的敏感詞過濾系統,是否比單模式串匹配方法更高效呢?
首先,我們需要將敏感詞構建成 AC 自動機,包括構建 Trie 樹以及構建失敗指針。我們上一節講過,Trie 樹構建的時間複雜度是 O(m*len),其中 len 表示敏感詞的平均長度,m
表示敏感詞的個數。那構建失敗指針的時間複雜度是多少呢?我這裏給出一個不是很緊確的上界。假設 Trie 樹中總的節點個數是 k,每個節點構建失敗指針的時候,(你可以看下代碼)最耗時的環節是 while 循環中的 q=q->fail,每運行一次這個語句,q 指向節點的深度都會減少 1,而樹的高度最高也不會超過 len,所以每個節點構建失敗指針的時間複雜度是 O(len)。整個失敗指針的構建過程就是 O(k*len)。不過,AC 自動機的構建過程都是預先處理好的,構建好之後,並不會頻繁地更新,所以不會影響到敏感詞過濾的運行效率。
再來看下,用 AC 自動機做匹配的時間複雜度是多少?

跟剛剛構建失敗指針的分析類似,for 循環依次遍歷主串中的每個字符,for 循環內部最耗時的部分也是 while 循環,而這一部分的時間複雜度也是 O(len),所以總的匹配的時間複雜度就是O(n*len)。因爲敏感詞並不會很長,而且這個時間複雜度只是一個非常寬泛的上限,實際情況下,可能近似於 O(n),所以 AC 自動機做敏感詞過濾,性能非常高。

你可以會說,從時間複雜度上看,AC 自動機匹配的效率跟 Trie 樹一樣啊。實際上,因爲失效指針可能大部分情況下都指向 root 節點,所以絕大部分情況下,在 AC 自動機上做匹配的效率要遠高於剛剛計算出的比較寬泛的時間複雜度。只有在極端情況下,如圖所示,AC 自動機的性能纔會退化的跟 Trie 樹一樣。
 

小結

多模式串匹配算法,AC 自動機。單模式串匹配算法是爲了快速在主串中查找一個模式串,而多模式串匹配算法是爲了快速在主串中查找多個模式串。
AC 自動機是基於 Trie 樹的一種改進算法,它跟 Trie 樹的關係,就像單模式串中,KMP 算法與BF 算法的關係一樣。KMP 算法中有一個非常關鍵的 next 數組,類比到 AC 自動機中就是失敗指針。而且,AC 自動機失敗指針的構建過程,跟 KMP 算法中計算 next 數組極其相似。所以,要理解 AC 自動機,最好先掌握 KMP 算法,因爲 AC 自動機其實就是 KMP 算法在多模式串上的改造。
整個 AC 自動機算法包含兩個部分,第一部分是將多個模式串構建成 AC 自動機,第二部分是在AC 自動機中匹配主串。第一部分又分爲兩個小的步驟,一個是將模式串構建成 Trie 樹,另一個是在 Trie 樹上構建失敗指針。

一、單模式串匹配:
1. BF: 簡單場景,主串和模式串都不太長, O(m*n)
2. KP: 字符集範圍不要太大且模式串不要太長, 否則hash值可能衝突,O(n)
3. naive-BM:模式串最好不要太長(因爲預處理較重),比如IDE編輯器裏的查找場景; 預處理O(m*m), 匹配O(n), 實現較複雜,需要較多額外空間.
4. KMP:適合所有場景,整體實現起來也比BM簡單,O(n+m),僅需一個next數組的O(n)額外空間;但統計意義下似乎BM更快,

5. 另外查資料的時候還看到一種比BM/KMP更快,且實現+理解起來都更容易的的Sunday算
法,有興趣的可以看這裏:
http://www.inf.fh-flensburg.de/lang/algorithmen/pattern/sundayen.htm
https://www.jianshu.com/p/2e6eb7386cd3
二、多模式串匹配:
1. naive-Trie: 適合多模式串公共前綴較多的匹配(O(n*k)) 或者 根據公共前綴進行查找(O(k))
的場景,比如搜索框的自動補全提示.
2. AC自動機: 適合大量文本中多模式串的精確匹配查找, 可以到O(n).

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章