前言
下文的代碼有些部分參考了這篇文章,但我仍然堅持作爲原創而非轉載,自有我的考慮。
在看下文之前,需要理解的基礎知識有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)))
後文
有一點要說的是,該算法構建字典樹的時間遠大於查找所用的時間,而且有一點我沒做好的是提供增量更新字典的方法,原因是懶~~