1. 程式人生 > >KMP演算法的簡單理解 【筆記】

KMP演算法的簡單理解 【筆記】

//本文除實現程式碼外全部為原創內容 轉載請註明出處  程式碼來自這裡

kmp演算法是一種改進的字串匹配演算法,由D.E.Knuth與V.R.Pratt和J.H.Morris同時發現,故稱KMP演算法

字串匹配:從字串T中尋找字串P出現的位置(P遠小於T)。其中P稱為“模式”。

KMP演算法對模式串進行O(m)的預處理後只需對文字T掃描一次即可找到匹配,所以時間複雜度為O(n+m)。


先不管O(m)的預處理,直接看O(n)的掃描。

對於下圖的字串匹配:(圖1)

1 :i=0 , j=0 , 判斷T[i] != P[j],則 i++.

2 : i=1, j = 0 , T[i]==P[j] , 則i++,j++

--------------


3 : 依次判斷T[2...5] == P [1...4] , 此時i = 6, j = 5 . T[i] != P[j] . (圖2)

如果使用BF演算法, i 指標在此時匹配失敗後會回溯, 然後重新進行匹配 .

KMP演算法中則不對 i 指標進行回溯, 而是修改 j 指標. 此時執行操作 j = next[j] = 2 , 繼續進行匹配; 其中next[]陣列是通過O(m)的預處理求得的, 暫時不管

--------------


4:(圖3中, P2的 j 指標是圖2中的 j 指標)由於我們從T[1]開始匹配直到T[6]才出現匹配失敗的情況, 所以T[1...5]==P[0...4]. 很明顯圖中圈出的兩個串相等, 即 P[0 , 1] == T[1 , 2]

--------------


5:通過觀察我們又能發現, P[0 , 1] == P[3 , 4]; (這就是O(m)預處理出的next[]陣列中值的意義 , next[5] = 2 , 則P[0...2-1] == P[5-2...5-1]

同步驟4,由於直到T[6]才出現匹配失敗, 所以又P[3 , 4] == T[4 , 5] . 所以P[0 , 1] == T[4 , 5].

在對 j 指標進行 j = next[j] 操作後, P3 [0 , 1] 就對應著P2 [3 , 4]. 即P3[0 , 1] == T[4 , 5] . 所以KMP演算法才會直接將j 指標跳到2而不是0 , 省去了對前一部分的匹配 , 也保證了不會漏掉匹配

6:P[0...5] == T[4...9] , 匹配成功.

--------------

在對next[]的預處理中 , 如果找不到一個像上面的P[2]一樣合適的位置 , 就只能從P[0]開始匹配 , 並且在當前的 i 位置不會找到匹配了 ,所以next = -1. 程式執行j = next[j] = -1後 , 在下一次迴圈中 判斷j==-1 就執行i++,j++. 通過這樣處理, 省去了 i 指標的回溯 , 降低了時間複雜度 . 因為這樣操作每次匹配無論成功還是失敗都會有 i++ , 所以時間複雜度是O(n)

C/C++ code:

int KMPMatch(char *s,char *p)
{
    int next[100];
    int i,j;
    i=0;
    j=0;
    getNext(p,next);	//預處理求next[]
    while(i<strlen(s))
    {
        if(j==-1||s[i]==p[j])
        {
            i++;
            j++;
        }
        else
        {
            j=next[j];       //消除了指標i的回溯
        }
        if(j==strlen(p))	//匹配成功
            return i-strlen(p);
    }
    return -1;
}

接下來再看O(m)的預處理。回顧一下剛剛的模式字串P(圖5


我們剛剛用到了j = next[j] = next[5] = 2, 當時j =5 , j-1 =4 .

觀察位置2的性質. 在子串P[0...j-1]即 P[01234] 中 , P[0]P[1] == P[3]P[4] 即 P[0...2-1] == P[5-2...5-1]

也就是說, 串P[0]P[1]即串P[3]P[4]既是字串P[0...4]的真字首, 又是它的真字尾. 重新看圖4就能明白 , 只要滿足這個性質, 就能保證語句 j = next[j] 的正確性.  

結論 若next[j]=k , 則k為滿足 P[0...k-1] == P[j-k ... j-1] 且 k<j 的最大值. 如果找不到這樣的k , next[j]=-1.

  顯然next[0]=-1 , next[1] = 0

預處理的方法(這裡只介紹遞推方法)

如果有next[j]=k, 則有P[0...k-1] == P[j-k ... j-1]

1) 如果P[j] == P[k]

 P[0...k-1] + P[k]  == P[j-k ... j-1] + P[j]   即  P[0...k] == P [j-k ... j]

所以next[j+1] = k + 1 = next[j] + 1

2) 如果P[j] != P[k], 那麼可以看做模式匹配的問題,匹配失敗的時候, 顯然k=next[k].

具體:(圖6)設next[j]=k, next[next[j]] = next[k] = k2


C/C++ code:

void getNext(char *p,int *next)
{
    int j,k;
    next[0]=-1;
    j=0;
    k=-1;
    while(j<strlen(p)-1)
    {
        if(k==-1||p[j]==p[k])    //匹配的情況下,p[j]==p[k]
        {
            j++;
            k++;
            next[j]=k;
        }
        else                   //p[j]!=p[k]
            k=next[k];
    }
}