KMP算法中next函數的理解


    首先要感謝http://blog.csdn.net/v_july_v/article/details/7041827以及http://blog.chinaunix.net/uid-27164517-id-3280128.html兩篇博文的作者,參考這兩篇博文才對KMP算法有了初步認識,本文的一些內容也是來自於這兩篇之中。KMP算法與BF算法的優略、回溯不回溯這些問題本文不作說明,而主要說明next函數(通常保存為一個next數組)的意義。這正是KMP算法難於理解的地方。

  為了方便起見,在不會起歧義的情況下做如下約定:下標都從0開始; 假設字符串為S,那么Si 表示第i個字符或者只有第i個字符的字符串;SiSi+1Si+2表示子串,如S1S2S3,S4S5等等;pre(S,i)表示字符串S的位置i之前的字符串,即S0S1...Si-1;next函數和next數組表示同一個意思。本文分為兩節,第一節講next函數的意義,得出需要滿足的兩個條件;第二節是具體代碼以及相關說明。

第一節  next函數的意義

    我們知道,KMP的基本匹配過程如下:在字符串T中查找模式P,需要記錄T中的當前位置以及P中的當前位置 j。當Ti=Pj的時候i和j都自增;當Ti!=Pj時,令j=next(j),然后繼續匹配。這樣就跳過了一些字符,而這些字符,本質上來講與字符串P0P1...Pj-1的前綴和后綴能匹配的最大長度相關。以圖一作解釋,來倒推next(j)的意義:

圖 1

    在T中查找P,P0P1P2P3P4=T1T2T3T4T5,但P5!=T6(i=6,j=5)。此時令j=next(j)。假設next[5]=2, 則跳過三個字符,新的j=2,從這個位置開始比較。能跳過的條件是什么?其一,從虛線部分可知必須保證P0P1=P3P4;其二,P5不能等於P2,因為之前我們知道了P5!=T6,如果P5=P2,那么P2肯定不等於T6。再次強調觀察虛線部分,發現P0P1和P3P4正好是字符P5前面的字符串(即P0P1P2P3P4)的前綴和后綴。

    圖1中,P5!=T6時跳過了三個字符,next[5]=2。再看圖2:next(5)分別等於4,3,1,0的情況。

圖 2

    仔細觀察圖2中的四種情況,均需要符合上面所說的條件。對於圖2的最后一幅圖,此種情況的條件是P0P1P2P3P4沒有相等的前綴和后綴,且P0!=P5。那么如果后一個條件不滿足呢,那么顯然P應該再移一個位置,對應的情況如圖3:

圖 3

    圖3中,next(5)=-1。因此next=-1的情況:Pj=P0,且P0P1...Pj-1沒有任何相等的前綴和后綴。另外,一般地,如果P0就發生失配,那么顯然i也要加一,因此next(0)=-1。

    在此作一小結,k=next(j)需要滿足的兩個條件如下:

    條件1. k是P0P1...Pj-1最長匹配的前綴和后綴的長度.

    條件2. Pj!=Pk.

第二節  next函數的求法

    利用以上知識,我們就知道求next函數的思路了。基本思路是利用上面的第一個條件(尋找最長匹配的前后綴),而第二個條件(Pj!=Pnext(j))則作為優化。這樣一步步理解會對算法思路更清晰一點。

基本思路: 利用條件1

使用歸納法:假設next(j)=k,則P0P1...Pk-1=Pj-k...Pj-2Pj-1,那么next(j+1)有兩種情況:

1. 如果Pk=Pj,則P0P1...Pk=Pj-k...Pj-1Pj,所以next(j+1)=k+1=next(j)+1。

2. 如果Pk!=Pj,這是可以看做另外一個字符串匹配的問題,主串和模式串都是p,當匹配失敗時,k=next(k)。

因此得到如下算法:

void get_nextval(char const* ptrn, int plen, int* nextval)    
{    
    int i = 0;
    nextval[i] = -1;    
    int j = -1;    
    while( i < plen-1 )    
    {    
        if( j == -1 || ptrn[i] == ptrn[j] )   //對應情況1
        {    
            ++i;    
            ++j;    
            next(i)=j;
        }    
        else                                  //對應情況2
            j = nextval[j];    
    }    
}   

    上述算法中,ptrn是模式串,plen是模式串長度,nextval數組保存所有位置的next值。該算法不考慮條件2,因此有可能發生Pi=Pnext(i)這種情況。在缺少該條件的情況下也可以用於做字符串匹配。假設next(i)=k,當匹配到i失效時,i=next(i)=k,這時候肯定也失效,因此又尋找k對應的next值,這樣算法得以進行。

優化:利用條件2

比較常見的算法對情況1做了優化,如下:

 1 void get_nextval(char const* ptrn, int plen, int* nextval)    
 2 {    
 3     int i = 0;
 4     nextval[i] = -1;    
 5     int j = -1;    
 6     while( i < plen-1 )    
 7     {    
 8         if( j == -1 || ptrn[i] == ptrn[j] )   //對應情況1
 9         {    
10             ++i;    
11             ++j;    
12             if( ptrn[i] != ptrn[j] )
13                 nextval[i] = j;
14             else    
15                 nextval[i] = nextval[j];    
16         }    
17         else                                  //對應情況2
18             j = nextval[j];    
19     }    
20 }

  該版本的算法考慮了條件2,因此進入情況1的時候,next(i)!=j。我們可以考慮一條查詢鏈,如圖4:

    圖4

    假設現在剛剛運行完13行,得出next[i]=j。此時必然有ptrn[i]!=ptrn[j]。因此下個循環的時候會跳轉到18行。該next鏈一直往前搜尋,直到某個位置k,ptrn[k]與ptrn[i]相等。該k就是最新的j值,這樣回到情況1,接着按照條件1優化。另外,當j==-1也應當進入情況1,因為不能往前搜尋了。

    以上就是next數組的求解過程,往后就可以利用next數組進行字符串查找了。在寫查找算法的過程中,可以發現與求next數組的算法過程驚人的一致。這也是KMP算法的一個特點,把兩者結合起來,更能夠理解它的奧妙所在。

 

 

 

 


免責聲明!

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



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