[译] Swift 算法学院 - KMP 字符串搜索算法

本篇是来自 Swift 算法学院的翻译的系列文章,Swift 算法学院 致力于使用 Swift 实现各类算法,对想学习算法或者复习算法的同窗很是有帮助,讲解思路很是清楚,每一篇都有详细的例子解释。 更多翻译的文章还能够查看这里html

Knuth-Morris-Pratt 字符串搜索算法

目标:用 Swift 写一个线性的字符串搜索算法,返回模式串匹配到全部索引值。git

换句话说就是,实现一个 String 的扩展方法 indexesOf(Pattern:String) ,函数返回 [Int] 表示模式串搜索到的全部索引值,若是没有匹配到,返回 nilgithub

举例以下:算法

let dna = "ACCCGGTTTTAAAGAACCACCATAAGATATAGACAGATATAGGACAGATATAGAGACAAAACCCCATACCCCAATATTTTTTTGGGGAGAAAAACACCACAGATAGATACACAGACTACACGAGATACGACATACAGCAGCATAACGACAACAGCAGATAGACGATCATAACAGCAATCAGACCGAGCGCAGCAGCTTTTAAGCACCAGCCCCACAAAAAACGACAATFATCATCATATACAGACGACGACACGACATATCACACGACAGCATA"
dna.indexesOf(ptnr: "CATA")   // Output: [20, 64, 130, 140, 166, 234, 255, 270]

let concert = "🎼🎹🎹🎸🎸🎻🎻🎷🎺🎤👏👏👏"
concert.indexesOf(ptnr: "🎻🎷")   // Output: [6]
复制代码

Knuth-Morris-Pratt 算法被公认是字符串匹配查找的最好算法之一。虽然 Boyer-Moore 简单,也一样只须要线性的时间复杂度。swift

这个算法后的思想和原来的 暴力字符串搜索算法 没什么不一样,KMP 和它一样将字符串从左到右依次比较,可是与之不一样的是不会在字符串不匹配时移动一个字符,而是用了更聪明的方式移动模式串。实际上这个算法对模式串特征作了预处理,使得它得到足够的信息能跳过没必要要的比较,因此能够移动更多的距离。数组

预处理后获得一个整型数组(代码中命名为 suffixPrefix),数组每一个元素 suffixPrefix[i] 记录的是 P[0...i]P 是模式串 )最长的的后缀等于其前缀的长度。换句话说,suffixPrefix[i]Pi 位置结束的最长子字符串就是 P 的一个前缀。(译者注:前缀指除了最后一个字符之外,一个字符串的所有头部组合;后缀指除了第一个字符之外,一个字符串的所有尾部组合。前缀和后缀的最长的共有元素的长度就是 suffixPrefix 要存的值)。好比 P = "abadfryaabsabadffg",则 suffixPrefix[4] = 0subffixPrefix[9] = 2subffixPrefix[14] = 4。(译者注:以 suffixPrefix[9] 为例,计算子字符串 abadfryaab , 其前缀集合为 a, ab,aba,abad,abadf,abadfr,abadfry,abadfrya,abadfryaa 和后缀集合为 b,ab,aab,yaab,ryaab,fryaab,dfryaab,adfryaab,badfryaab,相同的有 ab,由于匹配的只有一个,也就是最长值了,其长度为 2 ,所以 subffixPrefix[9] = 2。)计算这个并不复杂,可使用以下的代码实现:app

for patternIndex in (1 ..< patternLength).reversed() {
    textIndex = patternIndex + zeta![patternIndex] - 1
    suffixPrefix[textIndex] = zeta![patternIndex]
}
复制代码

简单计算一下以索引值结束,以 i 开始的子字符串与 P 前缀是否匹配。把(匹配上的最长的)字符串长度赋值给suffixPrefix 数组的 Index 值 。函数

完成 suffixPrefix 偏移数组后,算法第一步就是尝试与模式串各个字符比较,若是比较成功,继续比较下一个,若是所有匹配,则直接移向下一段文本,不然须要将模式串右移,右移的位数根据 suffixPrefix ,它可以保证前缀 P[0…suffixPrefix[i]] 可以与对应的字符(后缀)相匹配(译者注:实际就是把后缀的位置替换为相同的前缀的位置)。经过这种方式能够大大减小匹配的次数,能够加快不少。学习

以下为 KMP 算法实现:ui

extension String {

    func indexesOf(ptnr: String) -> [Int]? {

        let text = Array(self.characters)
        let pattern = Array(ptnr.characters)

        let textLength: Int = text.count
        let patternLength: Int = pattern.count

        guard patternLength > 0 else {
            return nil
        }

        var suffixPrefix: [Int] = [Int](repeating: 0, count: patternLength)
        var textIndex: Int = 0
        var patternIndex: Int = 0
        var indexes: [Int] = [Int]()

        /* 预处理代码: 经过 Z-Algorithm 算法计算移动用的表*/
        let zeta = ZetaAlgorithm(ptnr: ptnr)

        for patternIndex in (1 ..< patternLength).reversed() {
            textIndex = patternIndex + zeta![patternIndex] - 1
            suffixPrefix[textIndex] = zeta![patternIndex]
        }

        /* 查询代码:查找模式串匹配值 */
        textIndex = 0
        patternIndex = 0

        while textIndex + (patternLength - patternIndex - 1) < textLength {

            while patternIndex < patternLength && text[textIndex] == pattern[patternIndex] {
                textIndex = textIndex + 1
                patternIndex = patternIndex + 1
            }

            if patternIndex == patternLength {
                indexes.append(textIndex - patternIndex)
            }

            if patternIndex == 0 {
                textIndex = textIndex + 1
            } else {
                patternIndex = suffixPrefix[patternIndex - 1]
            }
        }

        guard !indexes.isEmpty else {
            return nil
        }
        return indexes
    }
}
复制代码

下面让咱们解释一下上面的代码。若是 P = "ACTGACTA"suffixPrefix 的结果为 [0, 0, 0, 0, 0, 0, 3, 1] ,文本为 "GCACTGACTGACTGACTAG"。算法开始的比较过程以下,先比较 T[0]P[0]

1       
                0123456789012345678
text:           GCACTGACTGACTGACTAG
textIndex:      ^
pattern:        ACTGACTA
patternIndex:   ^
                x
suffixPrefix:   00000031
复制代码

比较后发现不匹配,下一步比较 T[1]P[0] ,不幸的是要检查模式串不一致,所以须要继续向右移动模式串,移动多少须要查询 suffixPrefix[1 - 1] 。若是值是 0 ,须要再比较 T[1]P[0] 。但仍是不匹配,因此咱们继续比较 T[2]P[0]

1      
                0123456789012345678
text:           GCACTGACTGACTGACTAG
textIndex:        ^
pattern:          ACTGACTA
patternIndex:     ^
suffixPrefix:     00000031
复制代码

此次有相同的字符了,但也是至相同到第 8 位置,不幸的是匹配的长度与模式串长度并不相同,所以不能认为是相同的,但仍是有办法的,咱们能够用 suffixPrefix 数组存的值,匹配的长度是 7, 查看 suffixPrefix[7-1] 的值是 3。这个信息告诉咱们 P 的前缀与 T[0...8] 的子字符串是有匹配。suffixPrefix 数组保证咱们模式串有两个子字符串是与之匹配的,所以不用再进行比较,咱们能够直接大幅向右移动模式串!

T[9]P[3] 从新比较。

1       
                0123456789012345678
text:           GCACTGACTGACTGACTAG
textIndex:               ^
pattern:              ACTGACTA
patternIndex:            ^
suffixPrefix:         00000031
复制代码

继续比较直到第 13 位置,发现 GA 不匹配。像上面那样,继续根据 suffixPrefix 数组进行右移。

1       
                0123456789012345678
text:           GCACTGACTGACTGACTAG
textIndex:                   ^
pattern:                  ACTGACTA
patternIndex:                ^
suffixPrefix:             00000031
复制代码

再次进行比较,此次咱们终于找到一个,从位置 17 - 7 = 10

1       
                0123456789012345678
text:           GCACTGACTGACTGACTAG
textIndex:                       ^
pattern:                  ACTGACTA
patternIndex:                    ^
suffixPrefix:             00000031
复制代码

算法再继续比较 T[18]P[1],(由于 suffixPrefix[8 - 1] = 1),可是并不相同,在下次循环后也就中止计算了。

预处理阶段只涉及到模式串,运行 Z-Algorithm 算法是线性的,只须要 o(n),这里 nP 的模式串长度。完成后,在查找阶段复杂度也不会超出文本 T 长度(设为 m )。能够证实查找阶段的比较次数边界为 2 * m。因此 KMP 算法复杂度为 O(n + m)

注意:若是你要执行 KnuthMorrisPratt.swift 须要拷贝 Z-Algorithm 文件夹下的 ZAlgorithm.swiftKnuthMorrisPratt.playground 已经包含 Zeta 函数。

声明:这段代码是基于 1997年 CUP Dan Gusfield 的《Algorithm on String, Trees and Sequences: Computer Science and Computational Biology》 手册。

做者 Matteo Dunnhofer,译者 KeithMorning

译者注:因为本文原文在分析 KMP 算法上面明显不够用(虽然加了好多注释,😓),关键的 Next 数组算法又没说明白,想继续挖坑的同窗,推荐如下三篇文章,绝对够用。

相关文章
相关标签/搜索