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

相關文章
相關標籤/搜索