本篇是来自 Swift 算法学院的翻译的系列文章,Swift 算法学院 致力于使用 Swift 实现各类算法,对想学习算法或者复习算法的同窗很是有帮助,讲解思路很是清楚,每一篇都有详细的例子解释。 更多翻译的文章还能够查看这里。html
目标:用 Swift 写一个线性的字符串搜索算法,返回模式串匹配到全部索引值。git
换句话说就是,实现一个 String
的扩展方法 indexesOf(Pattern:String)
,函数返回 [Int]
表示模式串搜索到的全部索引值,若是没有匹配到,返回 nil
。github
举例以下:算法
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]
是 P
以 i
位置结束的最长子字符串就是 P
的一个前缀。(译者注:前缀指除了最后一个字符之外,一个字符串的所有头部组合;后缀指除了第一个字符之外,一个字符串的所有尾部组合。前缀和后缀的最长的共有元素的长度就是 suffixPrefix
要存的值)。好比 P = "abadfryaabsabadffg"
,则 suffixPrefix[4] = 0
,subffixPrefix[9] = 2
,subffixPrefix[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 位置,发现 G
和 A
不匹配。像上面那样,继续根据 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)
,这里 n
是 P
的模式串长度。完成后,在查找阶段复杂度也不会超出文本 T
长度(设为 m
)。能够证实查找阶段的比较次数边界为 2 * m
。因此 KMP 算法复杂度为 O(n + m)
。
注意:若是你要执行 KnuthMorrisPratt.swift 须要拷贝 Z-Algorithm 文件夹下的 ZAlgorithm.swift。 KnuthMorrisPratt.playground 已经包含
Zeta
函数。
声明:这段代码是基于 1997年 CUP Dan Gusfield 的《Algorithm on String, Trees and Sequences: Computer Science and Computational Biology》 手册。
做者 Matteo Dunnhofer,译者 KeithMorning
译者注:因为本文原文在分析 KMP 算法上面明显不够用(虽然加了好多注释,😓),关键的 Next
数组算法又没说明白,想继续挖坑的同窗,推荐如下三篇文章,绝对够用。