KMP Algorithm 字符串匹配算法KMP小結


 

這篇小結主要是參考這篇帖子從頭到尾徹底理解KMP,不得不佩服原作者,寫的真是太詳盡了,讓博主產生了一種讀學術論文的錯覺。后來發現原作者是寫書的,不由得更加敬佩了。博主不才,嘗試着簡化一些原帖子的內容,希望能更通俗易懂一些。博主的帖子一貫秉持通俗易懂的風格,使得非CS專業的人士也能讀懂,至少博主自己是這么認為的-.-|||

KMP算法,全稱Knuth-Morris-Pratt算法,根據三個作者Donald Knuth、Vaughan Pratt、James H. Morris的姓氏的首字母拼接而成的。是一種字符串匹配的算法,用於在一個文本串S中查找模式串P的位置。在講解KMP算法之前,我們先來看暴力破解法是如何運作的,假如我們有一個文本串S和一個模式串P如下:

文本串: BBC_ABCDAB_ABCDABCDABDE

模式串: ABCDABD

那么我們首先來找模式串的第一個字母A在文本串出現的位置:

BBC_ABCDAB_ABCDABCDABDE
    ABCDABD

找到后,再來一一比較后面的字母,比較到模式串的D的位置,發現不匹配:

BBC_ABCDAB_ABCDABCDABDE
    ABCDABD

暴力破解的下一步是將模式串后移一步,繼續來匹配開頭的A

BBC_ABCDAB_ABCDABCDABDE
     ABCDABD

直到找到下一個A,然后開始往后一一比較:

BBC_ABCDAB_ABCDABCDABDE
        ABCDABD

后面的步驟就不一一列舉了,都是按這種方法來查找的,這種算法十分的不高效,時間復雜度是O(m*n),其中m和n分別是文本串和模式串的長度。當m和n都很大的時候,運算速度就會很慢,那么此時就有請KMP算法閃亮登場!!

我們再回到暴力破解方法中的一一比較后面的字母那一步,比較到模式串的D的位置,發現不匹配:

BBC_ABCDAB_ABCDABCDABDE
    ABCDABD

此時KMP算法並不是將模式串向右移動一位,而是向后移動四位,直接到這一步:

BBC_ABCDAB_ABCDABCDABDE
        ABCDABD

這樣文本串的遍歷位置並不會移回去,而是'_'直接跟'C'匹配,是不是很神奇,它怎么知道要跟模式串上的哪個字符相比呢,實際上是從next數組中查的值,再講解next數組之前,我們先來講一下最大前綴后綴公共元素。

所謂最大前綴后綴公共元素,就是模式串中最大且相等的前綴和后綴,比如aba,有長度為1的相同前綴后綴a,再比如,字符串acdac有長度為2的相同前綴后綴ac,那么我們可以寫出ABCDABD的每一位上的前綴后綴長度:

A   B   C   D   A   B   D
0   0   0   0   1   2   0

由於模式串的尾部可能有重復的字符,所以我們可以得出一個重要的結論:失配時,模式串向右移動的距離 = 已匹配字符數 - 失配字符的上一位字符所對應的最大長度值

我們之前是在字符'D'處失配的,上一位字符是'B',對應的最大長度是2,此時已經成功匹配了6個字符,那么我們就將模式串向右移動6-2=4位,並繼續匹配即可。

BBC_ABCDAB_ABCDABCDABDE
        ABCDABD

此時我們發現'_'和'C'不匹配,那么'C'的上一個字符'B'的最大長度為0,此時已經匹配了2個字符,所以模式串向右移動2-0=2位繼續匹配,得到:

BBC_ABCDAB_ABCDABCDABDE
          ABCDABD

此時發現'_'和'A'不匹配,'A'已經是第一個了,不需要查表了,此時將模式串向右移動一位:

BBC_ABCDAB_ABCDABCDABDE
           ABCDABD

發現此時模式串的首字母'A'匹配上了,然后就按順序一路往下匹配,直到最后一個'D'和'C'失配:

BBC_ABCDAB_ABCDABCDABDE
           ABCDABD

我們進行和之前相似的操作,上一位字符是'B',對應的最大長度是2,此時已經成功匹配了6個字符,那么我們就將模式串向右移動6-2=4位,並繼續匹配即可:

BBC_ABCDAB_ABCDABCDABDE
               ABCDABD

移動后發現模式串的首字母'A'匹配上了,然后就按順序一路往下匹配,最終完成模式串的匹配:

BBC_ABCDAB_ABCDABCDABDE
               ABCDABD

我們發現文本串中的遍歷位置始終沒有退后,一直都是在向前的,這樣使得其比暴力破解法節省了大量的時間,其時間復雜度為O(m+n),簡直碉堡了。讀到這里是不是有疑問,怎么算法都結束了,還沒next數組什么事呢,其實next數組和這里的最大前綴后綴公共元素長度數組是有關聯的,上面的方法在失配時,要找失配字符前一個字符的最大前綴后綴公共元素長度值,那么如果我們將最大前綴后綴公共元素長度數組整體右移一位,形成next數組,如下所示:

A   B   C   D   A   B   D
0   0   0   0   1   2   0
-1  0   0   0   0   1   2

上面的中間那行是之前的最大前綴后綴公共元素長度數組,我們將其整體右移一位,多出的位置補上一個-1,就變成了下面的一行。那么我們此時就直接找失配字符的next值就行了。於是我們就得到了新的結論:失配時,模式串向右移動的距離 = 失配字符所在位置 - 失配字符對應的next值。

讀到這里是不是對KMP算法的發明者佩服的五體投地,別着急,還剩最后一部分,就是用代碼來遞推計算next數組。對於next的數組的計算,可以采用遞推來算。根據上面的分析,我們知道如果模式串當前位置j之前有k個相同的前綴后綴,那么可以表示為next[j] = k,所以如果當模式串的p[j]跟文本串失配后,我們可以用next[j]處的字符繼續和文本串匹配,相當於模式串向右移動了j - next[j]位。那么問題就來了,如何求出next[j+1]的值呢,我們還是來看例子吧:

模式串:    A  B  C  D  A  B  C  E
next值:   -1  0  0  0  0  1  2  ?  
索引:             k           j

如上所示,模式串為"ABCDABCE",且j=6, k = 2,我們有next[j] = k,這表示j位置上的字符C之前的最大前后綴長度為2,即AB。現在我們要求next[j+1]的值,因為p[k] == p[j],所以next[j+1] = next[j] + 1 = k + 1 = 3。即字母E之前的最大前后綴長度為3,即ABC。

那么我們再來看p[k] != p[j]的情況下怎么處理,還是來看例子:

模式串:    A  B  C  D  A  B  D  E
next值:   -1  0  0  0  0  1  2  ?  
索引:             k           j

這個例子把上面例子中的第二個'C'換成了'D',所以字符'E'前面的相同后綴就不再是3了,所以我們希望在k前面找出個k'位置,使得p[k']為D,這樣next[j+1] = k' +1,但是這個例子中不存在這樣的'D',所以next[j+1] = 0。我們看一個能在前綴中找到'D'的例子:

模式串:    D  A  B  C  D  A  B  D  E
next值:   -1  0  0  0  0  1  2  3  ?  
索引:                k           j

這個例子上面例子的最前面加上了個'D',此時j = 7, k = 3了,我們有next[j] = k,這表示j位置上的字符3之前的最大前后綴長度為3,即DAB。要求next[j+1]的值,我們發現此時p[k] != p[j],然后我們讓k = next[k] = 0,此時p[0]是D,那么next[j+1] = k + 1 = 1了,這說明字母E之前的最大前后綴長度為1,即D。綜上所述,我們可以寫出next的生成函數如下:

 

vector<int> getNext(string p) {
    int n = p.size(), k = -1, j = 0;
    vector<int> next(n, -1);
    while (j < n - 1) {
        if (k == -1 || p[j] == p[k]) {
            ++k; ++j;
            next[j] = k;
        } else {
            k = next[k];
        }
    }
    return next;
}

 

上面這種計算next數組的方式可以進一步的優化,可以優化的原因是因為上面的方法存在一個小小的問題,如果用這種方法求模式串ABAB,會得到next數組為[-1 0 0 1],我們用這個模式串去匹配ABACABABC:

ABACABABC
ABAB

我們會發現C和B失配,那么根據上面的規則,我們要向右移動j - next[j] = 3 - 1 = 2位,於是有:

ABACABABC
  ABAB

我們右移兩位后發現又是C和B失配了,而我們在上一步中,已知p[3] = B, s[3] = C,就已經失配了,讓p[next[3]] = p[1] = B再去和s[3]比較,肯定還是失配。原因是當p[j] != s[i]時,下一步要用p[next[j]]和s[i]去匹配,而如果p[j] == p[next[j]]了,再用p[next[j]]和s[i]去匹配必然會失配。所以我們要避免出現p[j] == p[next[j]]的情況,一旦出現了這種情況,我們可以再次遞歸,next[j] = next[next[j]],修改后的代碼如下:

 

vector<int> getNext(string p) {
    int n = p.size(), k = -1, j = 0;
    vector<int> next(n, -1);
    while (j < n - 1) {
        if (k == -1 || p[j] == p[k]) {
            ++k; ++j;
            next[j] = (p[j] != p[k]) ? k : next[k];
        } else {
            k = next[k];
        }
    }
    return next;
}

 

講到這里,KMP算法的內容就完全講完了,原帖子中還有兩個擴展方法,這里就不講了,感覺能把上述內容吃透就很不容易了,下面貼上完整的KMP的代碼僅供參考:

 

#include <iostream>
#include <vector>

using namespace std;

vector<int> getNext(string p) {
    int n = p.size(), k = -1, j = 0;
    vector<int> next(n, -1);
    while (j < n - 1) {
        if (k == -1 || p[j] == p[k]) {
            ++k; ++j;
            next[j] = (p[j] != p[k]) ? k : next[k];
        } else {
            k = next[k];
        }
    }
    return next;
}

int kmp(string s, string p) {
    int m = s.size(), n = p.size(), i = 0, j = 0;
    vector<int> next = getNext(p);
    while (i < m && j < n) {
        if (j == - 1 || s[i] == p[j]) {
            ++i; ++j;
        } else {
            j = next[j];
        }
    }
    return (j == n) ? i - j : -1;
}

int main() {
    cout << kmp("BBC_ABCDAB_ABCDABCDABDE", "ABCDABD") << endl; // Output: 15
}

 

參考資料:

http://blog.csdn.net/v_july_v/article/details/7041827

 

轉載請注明出處:來自Grandyang的博客園:http://www.cnblogs.com/grandyang/p/6992403.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM