Golang实现AC自动机

706 阅读4分钟

AC自动机简介

AC自动机(Aho-Corasick Automaton)是一种用于多模式字符串匹配的算法。它可以在给定一组模式(关键词)和一个文本字符串时,高效地找出文本中出现的所有模式。

AC自动机的主要特点是它将所有的模式构建成一个有向无环图(DFA),以实现快速的模式匹配。下面是AC自动机的工作原理:

  1. 构建Trie树:首先,将所有模式按照前缀树(Trie)的形式构建起来。这样做的目的是在搜索过程中能够快速地匹配文本中的字符。
  2. 添加失败路径:在Trie树中,为每个节点添加一个指向它的失败节点的链接。失败节点是指在匹配失败时,自动机将跳转到的节点。这个过程称为"Failure Function"的构建,它确保在匹配过程中的失败情况下自动机可以尽快恢复并继续匹配。
  3. 搜索匹配:一旦AC自动机构建完成,就可以将文本字符串输入自动机并开始搜索匹配。自动机将按照文本字符串的顺序一个字符一个字符地进行匹配。如果在当前节点无法匹配字符,它将通过失败节点链接转移到下一个节点,直到匹配或到达根节点。
  4. 输出匹配结果:每当自动机匹配到一个模式的末尾时,它会输出一个匹配结果。这意味着它可以在文本中找到所有的模式出现,并返回它们的位置或执行自定义操作。

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()

image.png

初始化后生成了如图所示的结构

2. 添加pattern字符串

b.AddStrings([]string{"he", "she", "hers", "his"})

添加完后的前缀树b结构如下图所示

image.png

简化一点,树状结构如下图所示

image.png

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指针

image.png

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())
    }
}
​

参考链接

aho-corasick

如果本文介绍得不够清除,建议看下这个视频AC自动机