1. 程式人生 > >單模匹配之KMP演算法簡解

單模匹配之KMP演算法簡解

void BF(char *x, int m, char *y, int n) 
{
    int i, j;
    for (j = 0; j <= n - m; ++j) 
    {
        for (i = 0; i < m && x[i] == y[i + j]; ++i);
        if (i >= m)
            printf(j);
    }
}
簡單來說,BF演算法就是將模式串pattern與目的串text中的字元逐個進行比較,某一輪比較的中途發現有不匹配字元或者在該輪比較中最後發現了模式串時(此時輸出該輪比較的初始位置),將目的串的初始比較位置下移
一個字元,從而開始下一輪的比較,直到目的串的初始比較位置到達了n-m時整個演算法就結束了。BF演算法相當於一個列舉演算法,有點暴力破解的含義在裡面,其時間複雜度為O(n*m)。
KMP演算法也是一個從左向右進行匹配的演算法,當在將模式串pattern與目的串text匹配中的字元逐個進行比較的過程中發現有mismatch的時候,目的串text的下一個參與比較的字元並不回退,仍保持不變,只是將模式串向右移動幾個位置繼續進行字元的逐個匹配。接下來我們討論兩個問題:為什麼此時只是將模式串向右移動幾個位置就能繼續進行字元的逐個匹配?具體應該將模式串移動幾個位置?
對第一個問題的解釋:
假定T為目的串,P為模式串。如圖(a)所示,當我們對比到T[10]與P[6]時(字串的起始位置從1開始)發現不匹配,此時若按照BF演算法會將T[10]回退到T[6]進行下一輪的匹配,但在KMP演算法中我們並不回退,因為通過前面的T[5...9]與P[1...5]相匹配(T[5...9]=P[1...5])我們已經知道T[5...9]的具體內容。此時若只是簡單的將T[10]回退到T[6]再進行下一輪比較,因為T[6]=P[2]=b,而P[1]=a,T[6]肯定不等於P[1],從而導致這種回退的比較顯得多餘,最終導致的結果就是這種回退的比較會影響演算法的效率。那麼有沒有一種方式可以讓T[10]不回退,即下一次T與P的字元比較還是在T[10]出進行?有的,此時我們可以讓P向右移動幾個位置,使得移動位置後的P其某一字首仍能與T[10]前面的某些字元相匹配,如圖(b)所示,當將P向右移動兩個位置後T[7...9]=P[1...3]成立,此時我們就可以安心的將T[10]與P[4]進行比較了,因為T[7...9]與P[1...3]已經相匹配了。這種情況下因為T不回退,所以其效率更高。
對第二個問題的解釋:
上面對第一個問題進行了解釋,說得不太好,但大致思想就這樣。接下來我們解釋下P具體應該向右一定幾個位置呢。我們先假設T[i...i+q-1]=P[1...q],其中i是字元匹配到了的初始位置,相當於圖(a)中的5,即i=5,q是匹配到了的字元的個數,相當於圖(a)中的5,即q=5,綜合一下就是T[5...9]=P[1...5],這個等式在圖(a)中是成立的。然後我們假設P向右移動幾個位置後有P[1...k]=T[i+q-k...i+q-1],因為P是向右移動的,所以肯定有0<k<q,又由前面的T[i...i+q-1]=P[1...q]可知P[1...k]=P[q-k+1...q],即模式串P中前k個字元和後k個字元相互匹配(模式串P中可能有多個這樣的字首和字尾對,我們去最長的那一對,這樣可以避免在目的串中某些匹配到了的串被遺漏掉),如圖(b)所示有P[1...3]=P[3...5],此時的k就是我們想要的。此處我們定義一個next[m]陣列,m的值為模式串P的長度,對於0<i<m+1有next[i]=k,比如對於i=5,有k=3,所以next[5]=3。那麼對於這個next陣列我們應該如何計算呢,程式碼如下:
getNext(char *P)
    m<-length[P];
    next[1]<-0;
    k<-0;
    for q<-2 to m
        do while k>0 and P[k+1]!=P[q]
            do k<-next[k]
        if P[k+1]=P[q]
            then k<-k+1
        next[q]<-k
    return next
至於程式碼的意圖,可以自己去揣摩揣摩,還是挺有意思的。
KMP匹配演算法的程式碼如下:
KMP-MATCH(char *P,char *T)
    n<-length[T]
    m<-length[P]
    next<-getNext(char *P)
    q<-0
    for i<-1 to n
        do while q>0 and P[q+1]!=T[i]
            do q<-next[q]
        if P[q+1]=T[i]
            then q-<q+1
        if q==m
            then print i-m
            q<-next[q]
由以上兩段程式碼我們可以看出裡面的大致結構很相似,這是因為在getNext程式碼段本質上也是一個字串模式匹配的過程。