AC自动机简介
AC自动机(Aho-Corasick Automaton)是一种用于多模式字符串匹配的算法。它可以在给定一组模式(关键词)和一个文本字符串时,高效地找出文本中出现的所有模式。
AC自动机的主要特点是它将所有的模式构建成一个有向无环图(DFA),以实现快速的模式匹配。下面是AC自动机的工作原理:
- 构建Trie树:首先,将所有模式按照前缀树(Trie)的形式构建起来。这样做的目的是在搜索过程中能够快速地匹配文本中的字符。
- 添加失败路径:在Trie树中,为每个节点添加一个指向它的失败节点的链接。失败节点是指在匹配失败时,自动机将跳转到的节点。这个过程称为"Failure Function"的构建,它确保在匹配过程中的失败情况下自动机可以尽快恢复并继续匹配。
- 搜索匹配:一旦AC自动机构建完成,就可以将文本字符串输入自动机并开始搜索匹配。自动机将按照文本字符串的顺序一个字符一个字符地进行匹配。如果在当前节点无法匹配字符,它将通过失败节点链接转移到下一个节点,直到匹配或到达根节点。
- 输出匹配结果:每当自动机匹配到一个模式的末尾时,它会输出一个匹配结果。这意味着它可以在文本中找到所有的模式出现,并返回它们的位置或执行自定义操作。
AC自动机的优势在于它的高效性能和线性时间复杂度。相较于简单地遍历文本并逐个比较模式,AC自动机能够在一个步骤中同时处理多个模式,从而加快匹配速度。因此,它广泛应用于词法分析、字符串搜索、敏感词过滤等领域。
本文结束一种基于Golang实现的AC自动机,源码见:github.com/BobuSumisu/…
数据结构
state结构体用于记录节点的状态
type state struct {
id int64 //节点id
value byte //当前节点值
parent *state //记录当前节点的父节点
trans map[byte]*state //用于记录当前节点有哪些子节点
dict int64 //用于表示当前节点是模式串的尾部,且模式串的长度为dict
failLink *state //失败指针,用于指向当前节点的最长前缀
dictLink *state //如果该节点的失败指针指向一个pattern的末尾,则该指针指向该末尾节点
pattern int64 //用于记录是第几个pattern,只记录尾节点
}
1. 新建前缀树
b := aho.NewTrieBuilder()
初始化后生成了如图所示的结构
2. 添加pattern字符串
b.AddStrings([]string{"he", "she", "hers", "his"})
添加完后的前缀树b结构如下图所示
简化一点,树状结构如下图所示
3. build
trie := b.Build()
// Build constructs the Trie.
func (tb *TrieBuilder) Build() *Trie {
//采用DFS计算前缀树所有节点的失败指针
tb.computeFailLinks(tb.root)
//采用DFS计算dictLink
tb.computeDictLinks(tb.root)
/*创建一些全局变量,通过Trie返回。
这样可以方便地通过访问数组来匹配,避免访问树,数组的访问效率相对更高*/
numStates := len(tb.states)
dict := make([]int64, numStates)
trans := make([][256]int64, numStates)
failLink := make([]int64, numStates)
dictLink := make([]int64, numStates)
pattern := make([]int64, numStates)
for i, s := range tb.states {
dict[i] = s.dict
pattern[i] = s.pattern
for c, t := range s.trans {
trans[i][c] = t.id
}
if s.failLink != nil {
failLink[i] = s.failLink.id
}
if s.dictLink != nil {
dictLink[i] = s.dictLink.id
}
}
return &Trie{dict, trans, failLink, dictLink, pattern}
}
build结束后,树的结构如下:
红色是failLink指针,绿色是dickLink指针
4. 搜索文本串
matches := trie.MatchString("ahishers")
核心方法为
func (tr *Trie) Walk(input []byte, fn WalkFn) {
s := rootState
for i, c := range input {
t := tr.trans[s][c]
/* 字符c不是s节点的孩子节点 */
if t == nilState {
/* 从失败指针开始继续匹配 */
for u := tr.failLink[s]; u != rootState; u = tr.failLink[u] {
if t = tr.trans[u][c]; t != nilState {
break
}
}
if t == nilState {
if t = tr.trans[rootState][c]; t == nilState {
t = rootState
}
}
}
s = t
/*s节点是个pattern的尾部*/
if tr.dict[s] != 0 {
// 调用回调函数,保存匹配信息
if !fn(int64(i), tr.dict[s], tr.pattern[s]) {
return
}
}
/* s节点还是另一个pattern的尾部 */
if tr.dictLink[s] != nilState {
for u := tr.dictLink[s]; u != nilState; u = tr.dictLink[u] {
if !fn(int64(i), tr.dict[u], tr.pattern[u]) {
return
}
}
}
}
}
完整测试代码
package main
import (
"fmt"
aho "github.com/BobuSumisu/aho-corasick"
)
func main() {
b := aho.NewTrieBuilder()
b.AddStrings([]string{"he", "she", "hers", "his"})
trie := b.Build()
matches := trie.MatchString("ahishers")
fmt.Printf("Got %d matches.\n", len(matches))
for _, match := range matches {
fmt.Printf("Matched pattern %v %q at position %d.\n", string(match.Match()),
match.Pattern(), match.Pos())
}
}
参考链接
如果本文介绍得不够清除,建议看下这个视频AC自动机