Python實現AC自動機

前言

下文的代碼有些部分參考了這篇文章,但我仍然堅持作爲原創而非轉載,自有我的考慮。

在看下文之前,需要理解的基礎知識有KMP算法原理和字典樹數據結構理解。瞭解了上述內容就可以開始之後的旅程了。

原理簡析

AC自動機相比於字典樹結構僅僅是多了fail結點,指向其已匹配成功的前綴。其模式匹配與KMP算法一致。

引用百度百科的圖片,即sh後匹配e失敗,此時h其實是已經匹配成功的狀態,所以可以從74這個匹配成功h的結點之後繼續匹配下一字符。
在這裏插入圖片描述

代碼實現

python3的具體實現

# -*- coding:utf-8 -*-
"""
Description: AC自動機

@author: WangLeAi
@date: 2018/8/19
"""
from collections import defaultdict


class TrieNode(object):
    def __init__(self, value=None):
        # 值
        self.value = value
        # fail指針
        self.fail = None
        # 尾標誌:標誌爲i表示第i個模式串串尾,默認爲0
        self.tail = 0
        # 子節點,{value:TrieNode}
        self.children = {}


class Trie(object):
    def __init__(self, words):
        print("初始化")
        # 根節點
        self.root = TrieNode()
        # 模式串個數
        self.count = 0
        self.words = words
        for word in words:
            self.insert(word)
        self.ac_automation()
        print("初始化完畢")

    def insert(self, sequence):
        """
        基操,插入一個字符串
        :param sequence: 字符串
        :return:
        """
        self.count += 1
        cur_node = self.root
        for item in sequence:
            if item not in cur_node.children:
                # 插入結點
                child = TrieNode(value=item)
                cur_node.children[item] = child
                cur_node = child
            else:
                cur_node = cur_node.children[item]
        cur_node.tail = self.count

    def ac_automation(self):
        """
        構建失敗路徑
        :return:
        """
        queue = [self.root]
        # BFS遍歷字典樹
        while len(queue):
            temp_node = queue[0]
            # 取出隊首元素
            queue.remove(temp_node)
            for value in temp_node.children.values():
                # 根的子結點fail指向根自己
                if temp_node == self.root:
                    value.fail = self.root
                else:
                    # 轉到fail指針
                    p = temp_node.fail
                    while p:
                        # 若結點值在該結點的子結點中,則將fail指向該結點的對應子結點
                        if value.value in p.children:
                            value.fail = p.children[value.value]
                            break
                        # 轉到fail指針繼續回溯
                        p = p.fail
                    # 若爲None,表示當前結點值在之前都沒出現過,則其fail指向根結點
                    if not p:
                        value.fail = self.root
                # 將當前結點的所有子結點加到隊列中
                queue.append(value)

    def search(self, text):
        """
        模式匹配
        :param self:
        :param text: 長文本
        :return:
        """
        p = self.root
        # 記錄匹配起始位置下標
        start_index = 0
        # 成功匹配結果集
        rst = defaultdict(list)
        for i in range(len(text)):
            single_char = text[i]
            while single_char not in p.children and p is not self.root:
                p = p.fail
            # 有一點瑕疵,原因在於匹配子串的時候,若字符串中部分字符由兩個匹配詞組成,此時後一個詞的前綴下標不會更新
            # 這是由於KMP算法本身導致的,目前與下文循環尋找所有匹配詞存在衝突
            # 但是問題不大,因爲其標記的位置均爲匹配成功的字符
            if single_char in p.children and p is self.root:
                start_index = i
            # 若找到匹配成功的字符結點,則指向那個結點,否則指向根結點
            if single_char in p.children:
                p = p.children[single_char]
            else:
                start_index = i
                p = self.root
            temp = p
            while temp is not self.root:
                # 尾標誌爲0不處理,但是tail需要-1從而與敏感詞字典下標一致
                # 循環原因在於,有些詞本身只是另一個詞的後綴,也需要辨識出來
                if temp.tail:
                    rst[self.words[temp.tail - 1]].append((start_index, i))
                temp = temp.fail
        return rst


if __name__ == "__main__":
    test_words = ["不知", "不覺", "忘了愛"]
    test_text = """不知、不覺·間我~|~已經忘了愛❤。"""
    model = Trie(test_words)
    # defaultdict(<class 'list'>, {'不知': [(0, 1)], '不覺': [(3, 4)], '忘了愛': [(13, 15)]})
    print(str(model.search(test_text)))

後文

有一點要說的是,該算法構建字典樹的時間遠大於查找所用的時間,而且有一點我沒做好的是提供增量更新字典的方法,原因是懶~~

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