【轉載】多模式串匹配之AC自動機

原文地址:http://www.javashuo.com/article/p-ynfymamt-e.htmlhtml

 

1、概述

AC自動機算法全稱 Aho-Corasick算法,是一種字符串多模式匹配算法。該算法在1975年產生於貝爾實驗室,是著名的多模匹配算法之一。AC算法用於在一段文本中查找多個模式字符串,即給你不少字符串,再給你一段文本,讓你在文本中找這些串是否出現過,出現過多少次,分別在哪裏出現。
該算法應用有限自動機巧妙地將字符比較轉化爲了狀態轉移。此算法有兩個特色,一個是掃描文本時徹底不須要回溯,另外一個是時間複雜度爲 O(n),時間複雜度與關鍵字的數目和長度無關,但所需時間和文本長度以及全部關鍵字的總長度成正比。
AC算法有三個主要步驟,一個是字典樹tire的構造,一個是搜索路徑的肯定(即構造失敗指針),還有就是模式匹配過程。
學習 AC自動機算法以前,最好先熟悉KMP算法,由於KMP算法與字典樹tire的構造非常相似。KMP算法是一種經典的單字符串匹配算法。

2、AC算法思想

 
AC算法思想:用多模式串創建一個肯定性的樹形有限狀態機,以主串做爲該有限狀態機的輸入,使狀態機進行狀態的轉換,當到達某些特定的狀態時,說明發生模式匹配。
下圖是多模式he/ she/ his /hers構成的一個肯定性有限狀態機,作幾點說明:
AC算法
圖2.1
一、 該狀態機優先按照實線標註的狀態轉換路徑進行轉換,當全部實線標註的狀態轉換路徑條件不能知足時,按照虛線的狀態轉換路徑進行狀態轉換。如:狀態0時,當輸入h,則轉換到狀態1;輸入s,則轉換到狀態3;不然轉換到狀態0。
二、 匹配過程以下:從狀態0開始進行狀態轉換,主串做爲輸入。如主串爲:ushers,狀態轉換的過程是這樣的:
AC算法
圖2.2
三、 當狀態轉移到2,5,7,9等紅色狀態點時,說明發生了模式匹配。
如主串爲:ushers,則在狀態五、二、9等狀態時發生模式匹配,匹配的模 式串有she、he、hers。
定義:
在預處理階段,AC自動機算法創建了三個函數,轉向函數 goto,失效函數failure和輸出函數output,由此構造了一個樹型有限自動機。
轉向函數,指的是一種狀態之間的轉向關係。g(pre, x)=next:狀態pre在輸入一個字符x後轉換爲狀態next(上圖中的實線部分)。若是在模式串中不存在這樣的轉換,則next= failstate
失效函數,指的也是狀態和狀態之間一種轉向關係。f(per)=next:是在比較失配的狀況下使用的轉換關係。在構造轉向函數時,把不存在的轉換用 failstate表示,可是 failstate不是一個具體的狀態,狀態機轉換轉換到 failstate狀態的時候就不知道該往哪轉了。因此就要在狀態機中找到一個有意義的狀態代替 failstate,當出現 failstate狀態時,自動切換到那個狀態。
這個狀態節點應該具備這樣的特徵:從這個狀態節點向上直到樹根節點(狀態0)所經歷的輸入字符,和從產生 failstate狀態的那個狀態節點向上所經歷的輸入字符串徹底相同。並且這個狀態節點,是全部具有這些條件的節點中深度最大的那個節點。若是不存在知足條件的狀態節點,則失效函數爲0。
累死了。舉例子說吧,對狀態9輸入任何一個字符都會產生 failstate狀態,須要失效函數。狀態3向上到狀態0通過的輸入字符串爲s;而由狀態9向上的輸入字符串爲 sreh。字符串s相同,而且狀態3是知足此條件的惟一節點,則
f(9)=3。
說來講去,失效函數就是要幹這麼件事兒:
AC算法
圖2.3
意思就是說,在比較模式串1發生失配時,找一個模式串2,使得P 2[0… j-1] = P 1[ i-j+1i]。而後繼續比較模式串2。看上面那個圖,想起點兒什麼東西沒有?對了,是KMP算法。有人說AC算法就是KMP算法在多模式匹配狀況下的擴展。
輸出函數,指的是狀態和模式串之間的一種關係。output(i)={P},表示當狀態機到達狀態i時,模式串集合{P}中的全部模式串可能已經完成匹配。

 

例:
模式串爲:he/ she/ hers/ his 時,如圖2.4所示。
轉向函數:
AC算法
圖2.4
失效函數:
AC算法
圖2.5
輸出函數:
AC算法
圖2.6
 

3、字典樹tire的構造

這個比較好理解,就是把要匹配的一些字符串添加到樹結構中去。樹邊就是單詞中的字符,單詞中最後一個字符的鏈接節點添加標誌,以表示改節點路徑包含1個字典中的字符串,搜索到此節點就表示找到了字典中的某個單詞,能夠直接輸出。
Trie是一個樹形結構的狀態裝換圖,從一個結點到它的各個子結點的邊上有不一樣的標號。 Trie的葉子結點表示識別到的關鍵字。
當咱們的模式串在 Tire上進行匹配時,若是與當前節點的關鍵字不能繼續匹配的時候,就應該去當前節點的失敗指針所指向的節點繼續進行匹配。
例子:某字典P={ he,she,his,hers}對應的字典樹以下圖:
AC算法
圖3.1
圖中有數字的節點到根節點的路勁正好對應字典中的字符串,數字表述單詞在字典中的順序,也能夠是其餘標誌。

4、搜索路徑的肯定

個人理解是:利用後綴字符串來肯定。後綴字符串就是某個字符串的後面的一部分。好比 abcde的後綴字符串有 bcde,cde,de和e。
假定目標字符串爲ushers,字典爲上圖(圖1)所示。
搜索過程目標字符串指針指向的字符和字典中的字符會有如下幾種狀況:
a. 當前字符匹配。表示從當前節點沿着樹邊有一條路徑能夠到達目標字符,此時只需沿該路徑走向下一個節點繼續匹配便可,目標字符串指針移向下個字符繼續匹配;
如:當指針指到s處,此時字典樹指針處於根,要從根到s處,能夠看到圖中有一條從根經s鏈接到的節點,所以字典樹節點指針指向此節點,目標字符串指針移動到下一字符h繼續匹配;顯然當前節點有一條經h鏈接到的節點,因而重複操做到有數字標誌的節點2處,表示已找到,該匹配字符串就是"she",輸出該字符串的位置後,目標字符串指針增1指向"r",字典指針指向數字2節點,進行下次匹配。
b. 當前字符無匹配。表示當前節點的任何一條邊都沒法達到要匹配的字符,此時不能沿現有路徑前進,只能回溯,回溯到存在的最長的後綴字符串處,若是沒有任何後綴字符串匹配則回溯到樹根處。而後從當前回溯節點判斷是否能夠到達目標字符串字符。
如:接上,因爲數字2節點無經"r"的鏈接,所以回溯,she的後綴字符串he在字典樹中,所以字典樹指針指向帶有數字1的標誌節點,因爲帶有標誌,直接輸出該節點"HE"(存疑,不少文章沒有提到此處須要輸出,正常路徑移動的字典指針節點要判斷是否能夠輸出,那麼由回溯路徑改變的字典指針指向的節點要不要判斷是否輸出?),而後從數字1節點判斷是否有經"r"到下一節點的路徑,顯然圖中有。所以字典樹節點指向下一節點,重複以上操做,最後找到"hers",此時匹配搜索也結束了。
以上兩種狀況直到目標字符串指針直到末尾結束匹配。在匹配過程當中遇到有標誌的節點說明找到了字典中的某個詞,能夠直接輸出。
 
輸出說明:
每次目標串指針移動前都須要判斷當前節點是否能夠輸出,並遞歸的判斷當前節點回溯路徑上的節點是否能夠輸出(其實就是判斷全部後綴字符串,she匹配時,其後綴he也會匹配,即便she不匹配,其後綴he也可能匹配,所以需遞歸判斷後綴字符串),直到樹根結束遞歸。
因爲固定字典的字符串的後綴字符串都是已知的,所以能夠在字典樹結構中存儲匹配失敗的路徑方向,所以只要字典樹構造完畢,就能夠根據字典樹的路徑進行匹配了,效率很是快。以上就是我對該算法的所有過程的理解,疏漏之處在所不免。

附錄:

1

含匹配失敗的狀況的路徑選擇的字典樹,實線表示匹配成功的正常路徑,虛線表示失敗的回溯路徑
AC算法圖 附1.1

2AC算法的僞代碼實現描述

T爲目標字符串,長度爲m,q爲字典樹的節點指針,g函數返回從節點q通過路徑T 到達的下一節點指針,f函數返回節點q的回溯節點指針。flag判斷節點是否爲標誌節點
q := 0; // initial state (root)
for i := 1 to m do
    while g( q,T) = NULL do
        q := f(q); //
回溯
    q := g(q,T); // 前進
    node:=q;
    while(node!=root){
        if flag(node) exist ; then print i, out(node);
        node = f(node);   //查找回溯節點
    }
end for;
 

附3

一個簡單的AC算法實現源碼示例參考:
/*
程序說明:多模式串匹配的AC自動機算法
自動機算法能夠參考《柔性字符串匹配》裏的相應章節,講的很清楚
*/

#include <stdio.h>
#include <string.h>

const  int MAXQ = 500000+10;
const  int MAXN = 1000000+10;
const  int MAXK = 26;  //自動機裏字符集的大小 
struct  TrieNode
{
    TrieNode* fail;
    TrieNode* next[MAXK];
    bool danger;   //該節點是否爲某模式串的終結點 
    int  cnt;    //以該節點爲終結點的模式串個數 
    TrieNode()
    {
        fail = NULL;
        memset(next, NULL, sizeof(next));
        danger = false;
        cnt = 0;
    }
}*que[MAXQ], *root;
//文本字符串
char  msg[MAXN];
int   N;
void  TrieInsert(char *s)
{
    int  i = 0;
    TrieNode *ptr = root;
    while(s)
    {
        int  idx = s-'a'; if(ptr->next[idx] == NULL) ptr->next[idx] = new TrieNode(); ptr = ptr->next[idx]; i++; } ptr->danger = true; ptr->cnt++; } void Init() { int i; char s[100]; root = new TrieNode(); printf("輸入模式串數量:"); scanf("%d", &N); for(i = 0; i < N; i++) { printf("輸入第%d個模式串(共%d個):",i,N); scanf("%s", s); TrieInsert(s); } } void Build_AC_Automation() { int rear = 1, front = 0, i; que[0] = root; root->fail = NULL; while(rear != front) { TrieNode *cur = que[front++]; for(i = 0; i < 26; i++) if(cur->next != NULL) { if(cur == root) cur->next->fail = root; else { TrieNode *ptr = cur->fail; while(ptr != NULL) { if(ptr->next != NULL) { cur->next->fail = ptr->next; if(ptr->next->danger == true) cur->next->danger = true; break; } ptr = ptr->fail; } if(ptr == NULL) cur->next->fail = root; } que[rear++] = cur->next; } } } int AC_Search() { int i = 0, ans = 0; TrieNode *ptr = root; while(msg) { int idx = msg-'a'; while(ptr->next[idx] == NULL && ptr != root) ptr = ptr->fail; ptr = ptr->next[idx]; if(ptr == NULL) ptr = root; TrieNode *tmp = ptr; while(tmp != NULL )&& tmp->cnt != -1) { ans += tmp->cnt; //統計文本中出現過的不一樣模式串數量 tmp->cnt = -1;//對於每一個模式串的出現只計算一次,如統計全部出現則應註釋該行 tmp = tmp->fail; } i++; } return ans; } int main() { int T; printf("輸入測試次數:"); scanf("%d", &T); while(T--) { Init(); Build_AC_Automation(); //文本 printf("輸入匹配文本:"); scanf("%s", msg); printf("%dn", AC_Search()); } getchar(); return 0; }

 下載:

 摘自snort的AC算法源碼實現等資料下載: 點擊下載此文件
相關文章
相關標籤/搜索