本篇是來自 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
數組算法又沒說明白,想繼續挖坑的同窗,推薦如下三篇文章,絕對夠用。