多模式串匹配 - AC 自动机

2019-11-10  本文已影响0人  微微笑的蜗牛

多模式串匹配概念

多模式串匹配,即多个模式串在一个主串中进行匹配。

虽然单模式串也能完成多模式串的匹配,但每个模式串都需要与主串进行匹配,如果遇到主串太长的情况,效率不高。

而多模式串匹配,只需要扫描一次主串,大大提高效率。

有一种经典的多模式串匹配算法 -- AC 自动机,全称是 Aho-Corasick 算法。下面来介绍其实现。

AC 自动机

AC 自动机Trie 树的基础上,增加了类似 KMPnext 数组,即每个节点会有个 fail 指针,指向某个节点。数据结构如下:

class ACNode {
    var data: Character
    
    // 字符集 26 个小写字母
    var children: [ACNode?]
    var isEnd: Bool = false
    var fail: ACNode? = nil
    var len: Int = 0
    
    init(_ data: Character) {
        self.data = data
        children = [ACNode]()
        
        // 初始化 26 个
        var i = 0
        while i < 26 {
            children.append(nil)
            i += 1
        }
    }
}

准备工作

  1. 将多个模式串构建成 Trie
  2. 构建 fail 指针

构建 Trie 树

参考 Trie 树 的文章。

构建 fail 指针

fail 指针的生成类似于 KMP 中的next 数组计算方式,基于前一个值来计算。

fail 指针的定义

每个节点都有 fail 指针。假设节点为 p,当前串 strrootp 的节点路径,找到其与所有模式串前缀匹配的最长后缀子串,那么 fail 就指向所匹配前缀的最后一个字符节点。

后缀子串:不包括开头字符的子串。

这里需要注意的是最长后缀子串,比如当前串为 abc,模式串为 bcc。虽然其与 bcc 都匹配,但最长的后缀子串是 bc

其中root.fail = NULL,若proot的直接子节点,则p.fail = root

fail 指针的生成方法

假设当前节点为 pp.fail = qpc 记为 p 的某个子节点,qc 记为 q 的某个子节点。

  1. pc == qc,则 pc.fail = qc。如下图所示:
image.png
  1. pc != qc,则不断循环 q = q.fail,直到找到 q 的子节点与 pc 相等,或者 q == root结束。但如果 q == root,则说明没有后缀与模式串的前缀匹配,此时则令 pc.fail = root。再继续这两个过程。
image.png

按照树的广度遍历,逐个生成节点的 fail 指针。

最终结果如下图所示:

image.png

Swift 代码如下:

// 构建失败指针
func buildFailPointer(_ root: ACNode) {
    var queue = Array<ACNode>()
    queue.append(root)
    
    while !queue.isEmpty {
        // 取出头部
        let p = queue.removeFirst()
        
        // 遍历其子节点
        if !p.children.isEmpty {
            for pc in p.children {
                if let pc = pc {
                    // 如果父节点是 root,则 fail 指针直接指向 root
                    if p === root {
                        pc.fail = root
                    } else {
                        var q = p.fail
                        while q != nil {
                            // 如果 pc 在 q 的子节点 qc 中存在,则直接指向 qc
                            let index = indexOfChar(pc.data)
                            if (index >= 0 && index <= q!.children.count) {
                                if let qc = q!.children[index] {
                                    pc.fail = qc
                                } else {
                                    // 不存在,找 q 的 fail 指针
                                    q = q!.fail
                                }
                            }
                        }
                        
                        // 不存在,则指向 root
                        if q == nil {
                            pc.fail = root
                        }
                    }
                    
                    queue.append(pc)
                }
            }
        }
    }
}

模式串匹配

首先需要明确以下两点:

  1. p 的节点路径 A ( root 到 p 的路径)匹配主串,则 p.fail 的节点路径 B 也是匹配的。因为 A 的后缀子串与B 的前缀是相同的,所以前面肯定匹配,同理 p.fail.fail 的节点路径也是。

    因此只需要不断遍历其 fail 指针节点,判断是否为结束符,如果是,则该模式串就是匹配主串的。

  2. 在某条分支路径上 A 做匹配,如果遇上不匹配的情况,则切到其 fail 指针指向的另外一条分支 B ,再继续匹配。因为其前后缀相同。

具体算法

逐个遍历主串的字符,判断该字符是否存在当前节点 p 的子节点中。

  1. 如果存在,则 p 指向其子节点,然后循环遍历 p 链式的 fail 指针指向的节点是否为模式串的结尾,若是,该模式串匹配完成。
  2. 如果不存在,则循环遍历 fail 的链式指针进行查找,若没找到,则节点 p 重新指回 root。重复这 2 个步骤。

Swift 代码如下:

// 进行匹配
func match(_ text: String, _ root: ACNode) {
    // 逐个遍历主串
    var p: ACNode? = root
    var i = 0
    while i < text.count {
        let strIndex = text.index(text.startIndex, offsetBy: i)
        
        let ch = text[strIndex]
        
        // 判断 p 的子节点是否匹配 ch,如果不匹配,则往 fail 指针找
        let index = indexOfChar(ch)
        
        while p?.children[index] == nil && p !== root {
            p = p?.fail
        }
        
        p = p?.children[index]
        
        // 一直没有匹配,重新指回 root
        if p == nil {
            p = root
        }
        
        // 遍历其 fail 指针,找到结束的字符,即为匹配
        var tmp = p
        while tmp != nil {
            if tmp!.isEnd {
                print("match startPosition:\(i - tmp!.len + 1)")
            }
            
            tmp = tmp?.fail
        }
        
        i += 1
    }
}
上一篇 下一篇

猜你喜欢

热点阅读