通俗易懂的講解KMP算法(字符串匹配算法)及代碼實現

jopen 9年前發布 | 26K 次閱讀 算法

一、本文簡介

本文的目的是簡單明了的講解KMP算法的思想及實現過程。

網上的文章的確有些雜亂,有的過淺,有的太深,希望本文對初學者是非常友好的。

其實KMP算法有一些改良版,這些是在理解KMP核心思想后的優化。

所以本文重點是講解KMP算法的核心,文章最后會有涉及一些改良過程。

二、KMP算法簡介

KMP算法是字符串匹配算法的一種。它以三個發明者命名,Knuth-Morris-Pratt,起頭的那個K就是著名科學家Donald Knuth。

三、KMP算法行走過程

首先我們先定義兩個字符串作為示例,被匹配串 S = "abcabdabcabeab",匹配串 T = "abcabeab"。

我們的目標就是確定S中是否包含T。

KMP算法的核心是分析匹配串 T 的特征,看看匹配串 T 能告訴我們什么信息。我把 T 著色,如下

T = "abcabeab"

現在看起來似乎比較明顯了,三個著色點都是重復的 "ab",似乎這個 T 能告訴我們它有重復的子串"ab"可以利用。那么它們到底怎么用?先不講具體怎么用,先走一遍KMP這個過程,但是大家需要留意這個"ab"。

1.

通俗易懂的講解KMP算法(字符串匹配算法)及代碼實現

我們發現T在匹配成功 "abcab" 后到"e"時和S子串的"d"匹配不成功了。

這個時候我們可以得到的先驗就是目前T匹配過的S子串,就是"abcab"。而T本身就是"ab"重復的。所以"abcab"可以直接跳到第二個重復"ab"的位置,因為"abcab"中其它字符串開頭不可能產生和T對應的匹配,這是很直觀的。

因此T應該直接后移三個位置,并且用第三位的"c"和S剛才不匹配的"d"進行比較。

通俗易懂的講解KMP算法(字符串匹配算法)及代碼實現

可以發現,S匹配的過程是不會回退的。因此匹配過程是S從頭到尾的一遍掃描(中間可能因為匹配成功退出),所以這個查找過程是O(N)的復雜度。

2.

此時的匹配串是 "ab", 它告訴我們目前匹配的是"ab"。這個不像"abcab"出現了重復"ab",所以我們知道包括S[5]的"d"之前的子串是垃圾串。因此跳過S[5]重新開始匹配T。

通俗易懂的講解KMP算法(字符串匹配算法)及代碼實現

最后完成匹配,可見,這樣一次對S的掃描完成了對T的查找。

那么對于機器如何實現?第四節會分析。

四、KMP算法核心解析

對于上面的過程,我們抽離出來的話,問題的根本就是對T串重復情況的一個判定。不管S是什么串,只要對T的構造模式分析清楚就可以完成上述跳轉過程。

因此需要一個數組記錄這個T的 模式函數。

這里先給出這個T的模式函數

通俗易懂的講解KMP算法(字符串匹配算法)及代碼實現

1.每個字母對應模式函數的值就是匹配到當前位置 i 后,下一次T開始進行比較的下標。

2.而S的移動長度為 i - F(i-1)。

對應上面兩個問題

1.比如上面的匹配到的 "abcab" ,是T匹配到 T[5] 即"e"的時候出錯的。那么我們需要查看上一個字符的模式函數值,因為上一個函數值才代表了已經匹配的串。

發現F(4)=2,說明下次比較從T[2]即"c"開始。因為"abcab"有重復"ab",第一個"ab"不需要比較。

2.而S下標移動多少呢?S下標的移動即找到T的初試位置對應的S的下標。對應上面第二張圖,S[3]和T的移動對應起來了。3的獲得就是通過上面公式 5-F(4)得到的。其實這個結果和1得到匹配位置的思路是一樣的,不過又向前移動對齊了開頭。

為什么如此構造這個模式函數?

就是因為F值表示了對于位置 i, T[i]有無重復,并且重復下標的位置在哪(F[i])。既然我們獲取了重復下標的位置,那么其它的相關值可以推出來了。

基于這個思路,再給出另外兩個T串的模式函數值,幫助大家思考。

1.

通俗易懂的講解KMP算法(字符串匹配算法)及代碼實現

2.

通俗易懂的講解KMP算法(字符串匹配算法)及代碼實現

如何快速構造這個模式函數?

這個留給大家思考一下了,應該也比較直接了,注意是查找重復位置。如果不大明白,可以參考下面的代碼。

五、KMP算法實現

 再添加個例子幫助大家思考:

S1 = "aaaaaaaaaaaaaaaab"  S2 = "aaaaafaaaaaaaaab"   T = "aaaab"

對于S1和S2兩種情況,應如何匹配。

代碼如下:

/*
    return val means the begin pos of haystack
    -1 means no matching substring
*/
int KMP(char *haystack, char *needle) {
    // pre-process
    if(haystack[0] == 0 && needle[0] == 0)
        return 0;

    int i, j, k, min, cur;

    //construct F(t) in vector len
    vector<int > len;
    len.push_back(0);
    for(i=1; needle[i] != 0; i++){
        if(len[i-1] == 0){
            if(needle[i] == needle[0])
                len.push_back(1);
            else
                len.push_back(0);
        }else{
            if(needle[i] == needle[len[i-1]])
                len.push_back(len[i-1]+1);
            else
                len.push_back(0);
        }
    }
    // KMP finder
    j = 0;
    for(i=0; haystack[i] != 0; ) {
        // matching
        for(; needle[j] != 0; j++) {
            if(haystack[i+j] != needle[j])
                break;
        }
        //finded
        if(needle[j] == 0)
            return i;
        else{ // jump
            if(j){
                cur = j - len[j-1];
                i += cur;
                j = len[j-1];
            }else{
                j = 0;
                i++;
            }
        }
    }
    //match failed
    return -1;
}

六、KMP算法改進

KMP算法有一些改進版本加速查找,一般可以通過S串中的一些信息加速匹配過程。

比如若  S = "aaaaaaaafaaaaaaaaaaaab", T = "aaaaaaab"。

在查找過程中,S中間的 "f" 起到了阻擋作用。但是由于我們只是考慮T的先驗信息,遇到"f" 不匹配會導致T每次后移一步進行新的匹配,直到T的開頭碰到了"f"。

但是如果我們加入"f"這個原串S的信息,由于"f" != "b" && "f" != "a" && i-1 = F(i-1) ,所以直接跳到"f"后進行新的匹配會更快速的查找。

但是這些改進都是基于KMP基礎算法之上的,因此把握核心要點不僅省時省力,更能有效擴展。

七、參考

[1] 《字符串匹配的KMP算法》 http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

[2] 算法導論

來自:http://www.cnblogs.com/xiaoboCSer/p/4237941.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!