字符串匹配算法 之 Aho-Corasick

簡介

首先簡要介紹一下AC自動機:Aho-Corasick automation,該算法在1975年產生于貝爾實驗室,是著名的多模匹配算法之一。一個常見的例子就是給出n個單詞,再給出一段包含m個字符的文章,讓你找出有多少個單詞在文章裏出現過。要搞懂AC自動機,先得有模式樹(字典樹)Trie和KMP模式匹配算法的基礎知識。AC自動機算法分爲3步:構造一棵Trie樹,構造失敗指針和模式匹配過程。

多模匹配

AC自動機(Aho-Corasick Automaton)是多模匹配算法的一種。所謂多模匹配,是指在字符串匹配中,模式串有多個。前面所介紹的KMP、BM爲單模匹配,即模式串只有一個。假設主串T[1m] ,模式串有kP=P1,,Pk ,且模式串集合的總長度爲n 。如果採用KMP來匹配多模式串,則算法複雜度爲:

O(|P1|+m++|Pk|+m)=O(n+km)

而KMP並沒有利用到模式串之間的重複字符結構信息,每一次的匹配都需要將主串從頭至尾掃描一遍。因此,貝爾實驗室的Aho與Corasick於1975年結合KMP與有限狀態機(finite state machines)的思想,提出AC自動機算法[1]。

AC算法

這裏寫圖片描述

思想

自動機按照文本字符順序,接受字符,併發生狀態轉移。這些狀態緩存了“按照字符轉移成功(但不是模式串的結尾)”、“按照字符轉移成功(是模式串的結尾)”、“按照字符轉移失敗”三種情況下的跳轉與輸出情況,因而降低了複雜度。

基本構造

AC算法中有三個核心函數,分別是:

  • success; 成功轉移到另一個狀態(也稱goto表或success表)
  • 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表的時候進行一次拓充。

這裏寫圖片描述

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。

這裏寫圖片描述

算法實現

# -*- encoding=utf-8 -*- 

__all__ = ['Ahocorasick', ]

class Node(object):

    def __init__(self):
        self.next = {}
        self.fail = None
        self.isWord = False

class Ahocorasick(object):

    def __init__(self):
        self.__root = Node()

    def addWord(self, word):
        '''
            @param word: add word to Tire tree 
                            添加關鍵詞到Tire樹中
        '''
        tmp = self.__root
        for i in range(0, len(word)):
            if not tmp.next.has_key(word[i]):
                tmp.next[word[i]] = Node()
            tmp = tmp.next[word[i]]
        tmp.isWord = True

    def make(self):
        '''
            build the fail function 
            構建自動機,失效函數
        '''
        tmpQueue = []
        tmpQueue.append(self.__root)
        while(len(tmpQueue) > 0):
            temp = tmpQueue.pop()
            p = None
            for k, v in temp.next.items():
                if temp == self.__root:
                    temp.next[k].fail = self.__root
                else:
                    p = temp.fail
                    while p is not None:
                        if p.next.has_key(k):
                            temp.next[k].fail = p.next[k]
                            break
                        p = p.fail
                    if p is None :
                        temp.next[k].fail = self.__root
                tmpQueue.append(temp.next[k])

    def search(self, content):
        '''
            @return: a list of tuple,the tuple contain the match start and end index
        '''
        p = self.__root
        result = []
        startWordIndex = 0
        endWordIndex = -1
        currentPosition = 0

        while currentPosition < len(content):
            word = content[currentPosition]
            # 檢索狀態機,直到匹配
            while p.next.has_key(word) == False and p != self.__root:
                p = p.fail

            if p.next.has_key(word):
                if p == self.__root:
                    # 若當前節點是根且存在轉移狀態,則說明是匹配詞的開頭,記錄詞的起始位置
                    startWordIndex = currentPosition
                # 轉移狀態機的狀態
                p = p.next[word]
            else:
                p = self.__root

            if p.isWord:
                # 若狀態爲詞的結尾,則把詞放進結果集
                result.append((startWordIndex, currentPosition))

            currentPosition += 1
        return result

    def replace(self, content):
        replacepos = self.search(content)
        result = content
        for i in replacepos:
            result = result[0:i[0]] + (i[1] - i[0] + 1) * u'*' + content[i[1] + 1:]
        return result


if __name__ == '__main__':
    ah = Ahocorasick()
    text = raw_input("text: ")
    patterns = raw_input("pattern: ")
    words = patterns.split(" ")
    for w in words:
        ah.addWord(w)
    ah.make()
    results = ah.search(text)
    print results
    if len(results) == 0:
        print "No find."
    else:
        print len(results)," matching results are listed below."
        print "-------" + "-"*len(text) + "-------"
        print text
        count = 0
        for site in results:
            w = text[site[0]:site[1]+1]
            count += 1
            print " "*site[0] + w + " "*(len(text)-site[1]) + "  " + str(site[0]) + "  " + str(count)
        print "-------" + "-"*len(text) + "-------"

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

參考

發佈了126 篇原創文章 · 獲贊 94 · 訪問量 46萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章