KMP字串匹配演算法理解(轉)

小小IT墨魚發表於2017-12-01
一、引言

主串(被掃描的串):S=‘s0s1...sn-1’,i 為主串下標指標,指示每回合匹配過程中主串的當前被比較字元;

模式串(需要在主串中尋找的串):P=‘p0p1...pm-1’,j 為模式串下標指標,指示每回合匹配過程中模式串的當前被比較字元。

字串匹配:在主串中掃描與模式串完全相同的部分,並返回其在主串中的位置,這裡的起始掃描位置預設為主串的第一個字元,即預設pos=0,其他情況類似。

樸素匹配演算法:在模式串與主串的匹配過程中,一共要進行n=Length(S)回合的匹配,每一回合分別從主串的起始字元s0,s1,...,sn-1開始進行。在具體某一回合的匹配過程中,每當模式串P中的某一字元與主串S中的被比較字元不相等,主串S的指標 i 都必須回溯到此回合起始字元的下一個位置,模式串P的指標 j 回到模式串串首,重新進行下一回合匹配。演算法最壞情況下的時間複雜度為O(m*n)。這裡不再詳述。

KMP匹配演算法:KMP是一個高效的字串匹配演算法,它是由三位計算機學者 D.E.Knuth 與 V.R.Pratt 和 J.H.Morris 同時發現的,因此人們通常簡稱它為 KMP 演算法。在KMP匹配過程中,每當模式串P中的某一字元與主串S中的被比較字元不相等,主串S的指標 i 不需要回溯,而只要將P串“向右滑動”到一定位置,繼續進行下一回合的比較。KMP演算法的時間複雜度為O(m+n)。

kmp的用處場景

1.查詢模式串是否在目標串

2.計算字串是由哪個或哪幾個字串迴圈而來

3.查詢模式串在目標串的哪些位置

4.最長公共子串

下面主要理解KMP匹配演算法。

1)先由KMP演算法的主要思想得到next函式的定義

2)然後根據next函式定義求取next函式值

3)最後根據next函式值進行主串、模式串匹配

附:根據next函式定義一眼看出來next函式值。

二、定義next[j]

我們要解決的關鍵問題是:當本回合匹配過程中出現失配時,下回合匹配時模式串“向右滑動”的可行距離有多遠,也即:本回合匹配過程中,主串S的第 i 個字元與模式串P中的第j個字元失配時,下回合匹配時主串中的第i個字元應與模式串中的哪個字元進行比較(設為next[j],因為是“向右滑動”,故next[j]<j)。

引例:假設有以下主串和模式串:

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P: a b a a b c a c              
j: 0 1 2 3 4 5 6 7              

a)。。。之前匹配步驟

b)在經過若干回合匹配之後,兩字串狀態如下(i=1,j=0):

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:   a  b a a b c a c            
j: -1  0 1 2 3 4 5 6 7            

c)此時主串的第1個字元與模式串的第0個字元不等,主串的指標i不回溯,由於next[j]<j,考慮定義:next[j] =next[0]= -1。模式串指標j向右移動至位置next[0]= -1,情形如下。(位置"j=-1"是假想的,在後面我們將會發現這樣處理的好處)

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:     a  b a a b c a c          
j:   -1 0 1 2 3 4 5 6 7          

d)此時,模式串再也無法向右滑動,此輪匹配失敗,令i和j均自增,繼續進行比較,情形如下(i=2,j=0):

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:     a  b a a b c a c          
j:   -1 0 1 2 3 4 5 6 7          

e)此時字元相等,主串的指標i和模式串的指標j均自增1,繼續匹配結果如下(i=3,j=1):

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:     a  b a a b c a c          
j:     0 1 2 3 4 5 6 7          

f)。。。如此重複e)

g)當i=7,j=5時,主串的與模式串當前字元不相等。情形如下(i=7,j=5):

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:     a  b a a b c a c          
j:     0 1 2 3 4 5 6 7          

此時,主串的指標i不回溯,那麼模式串將向右滑動至什麼位置呢?

我們假設next[j]=k(k<j)。

①本回合,si與pj失配(i=7,j=5),而在失配之前有“部分匹配”,故有:'p0p1...pj-1'='si-jsi-j-1...si-1',又因為k<j,從而'p0p1...pj-1'的部分串'pj-kpj-k+1...pj-1'滿足:'pj-kpj-k+1...pj-1'='si-ksi-k+1...si-1';

②下回合,因為要保證從模式串的k位置處字元開始比較,那麼必須保證模式串k位置之前的部分串'p0p1...pk-1'滿足:'p0p1...pk-1'='si-ksi-k+1...si-1',其中k取最大的可能值;

由①式、②式右半邊相等可知,k的取值滿足下面的判別等式:'pj-kpj-k+1...pj-1'='p0p1...pk-1',其中k取該解集中的儘可能大值。考慮定義:next[j] = Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}。

上式也即:j位置之前的P尾串 = 0位置(包括0)之後k位置之前的P頭串,如圖中:'p3p4'='p0p1',k=2,反映了模式串在j位置之前的P尾串 與 0位置(包括0)之後k位置之前的 P頭串的重複程度。

h)模式串向右滑動至位置next[j]=2,情形如下(i=7,j=2):

i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
S: a c a b a a b a a b c a c a a
P:           a  b a a b c a c    
j:           0 1 2 3 4 5 6 7    

j)。。。後續匹配

根據以上討論,對k(next[j])考慮如下定義:

(1)next[j] = -1(j=0);

(2)next[j] = Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}(j!=0且集合有解);

(3)next[j] = 0(j!=0且集合無解);

 

三、求取next[j]

根據next[j]的定義可知,此函式的值取決於模式串的本身以及模式串的失配位置。此時把求解next[j]問題看成是一個模式串自匹配問題即主串和模式串都是P,從主串P的p1開始與模式串P的p0開始進行匹配

當j=0時,由定義得:next[0]=-1;

當j!=0時,我們思路是:按照主串的下標j由小及大,依次求next[1]、next[2]、...、next[m-1],m為模式串的維數。現在假設已知next[j]=k,且next[t](t<j)均已求得。如果求得next[j+1]與next[j]的關係,那麼所有的next函式值均可被求出。

此時,由next[j]的定義可知:'pj-kpj-k+1...pj-1'='p0p1...pk-1',且k為最大值,下面分兩種情況討論:

(a)如果pj=pk,結合'pj-kpj-k+1...pj-1'='p0p1...pk-1'可以得到'pj-kpj-k+1...pj-1pj'='p0p1...pk-1pk',又不存在k'>k滿足該式,由next函式的定義可知:next[j+1]=k+1,也即:next[j+1]=next[j]+1。這個式子的意味著,該情況下主串字元指標(j+1)位置處的next[j+1]可以由當前j位置處的next[j]加1求得。(由於下標最小的next函式值next[1]=0是已知的,這使得按下標由小及大的順序求解所有next函式值成為可能,這種情況對應著下面虛擬碼的 if(P[i] == P[j])語句部分)

(b)如果pj!=pk,將模式串向右滑動至k'(k'=next[k]<k<j)位置,使得主串的pj字元與模式串的pk'字元比較。

①如果此時pj=pk'(k'<k),結合'pj-kpj-k+1...pj-1'='p0p1...pk-1',則有'pj-k'pj-k'+1...pj-1pj'='p0p1...pk'-1pk'',由next函式的定義該式等價於:next[j+1]=k'+1=next[k]+1(觀察下標k<j,由於next[t](t<=j)均為已知,則一定可以求出next[j+1])。

②如果此時pj!=pk',則將模式串繼續向右滑動,直至pj和模式串的某個字元pk_lucky匹配成功,此時pj=pk_lucky(k_lucky<k),結合'pj-kpj-k+1...pj-1'='p0p1...pk-1',則有'pj-k_luckypj-k_lucky+1...pj'='p0p1...pk_lucky',由next函式的定義該式等價於:next[j+1]=k_lucky+1=next[...next[k]...]+1(在幾次連續的滑動過程中,每次迭代k'=next[k],k'<k<j恆成立,由於next[t](t<=j)可知已知,則一定可以求出next[j+1])。

①和②的討論說明,無論經過多少次滑動,只要主串的pj最終與模式串pk_lucky字元匹配成功,則主串字元指標(j+1)位置處的next[j+1]一定可以由next[t](其中t<=j)加1求得(這種情況對應著下面虛擬碼的else語句部分)

③儘管向右滑動,一直到j=next[t]=-1,很不幸找不到k'使得pj=pk',這相當於匹配過程中無解,此時由定義知next[j+1]=0(這種情況對應著下面虛擬碼的if(j==-1)部分)

故虛擬碼如下:

void get_next(SString P,  int next[]){
    //求模式串P的next函式值並存入陣列next中,i、j分別代表主串、模式串的下標
    i = 0; j = -1; next[0] = -1;
    while(i < len(P)-1){
        if( j==-1 || P[i] == P[j] ) { ++i; ++j; next[i] = j; }//每當自增i和j,得到一個新的next[i]
        else j = next[j];//模式串向右移動
    }
}

例子:有以下模式串:
P: a b a a b c a c
j: 0 1 2 3 4 5 6 7

演算法的前幾次迭代過程列舉如下:

a)執行if,++i,++j,進入初始狀態(i=1,j=0),next[1]=0:

i: 0 1 2 3 4 5 6 7  
P: a b a a b c a c  
P:   a b a a b c a c
j: -1 0 1 2 3 4 5 6 7

b)執行else,模式串向右滑動(i=1,j=next[0]=-1):

i: 0 1 2 3 4 5 6 7    
P: a b a a b c a c    
P:     a b a a b c a c
j:   -1 0 1 2 3 4 5 6 7

c)j==-1,執行if,++i,++j(i=2,j=0),next[2]=0:

i: 0 1 2 3 4 5 6 7    
P: a b a a b c a c    
P:     a b a a b c a c
j:   -1 0 1 2 3 4 5 6 7

d)pi=pj,執行if,++i,++j(i=3,j=1),next[3]=1:

i: 0 1 2 3 4 5 6 7    
P: a b a a b c a c    
P:     a b a a b c a c
j:   -1 0 1 2 3 4 5 6 7

e)執行else,模式串向右滑動(i=3,j=next[1]=0):

i: 0 1 2 3 4 5 6 7      
P: a b a a b c a c      
P:       a b a a b c a c
j:     -1 0 1 2 3 4 5 6 7

以此類推。。。

 最後求得next[j]如下:

j: 0 1 2 3 4 5 6 7
P: a b a a b c a c
next[j] -1 0 0 1 1 2 0 1

 

四、改進的next函式值演算法

這樣的改進已經是很不錯了,但演算法還可以改進,注意到下面的匹配情況:

i 0 1 2 3 4 5 6 7 8
S a a a b a a a a b
P a a a a b        
j 0 1 2 3 4        
next[j] -1 0 1 2 3        
nextval[j] -1 -1 -1 -1 3        

模式串P中的p3='a'和主串S中的s3='b'失配時,P向右滑動至next[3]=2位置,由於p2=p3='a',此時的比較還是會失配;然後P向右滑動至next[2]=1位置,由於p1=p2='a',此時的比較還是會失配;P向右滑動至next[1]=0位置,由於p0=p1='a',此時的比較還是會失配,P向右滑動至next[0]=-1位置。

如果我們能夠修正next[1]=next[next[1]]=next[0]=-1,修正next[2]=next[next[2]]=next[1](修正後的值)=-1,修正next[3]=next[next[3]]=next[2](修正後的值)=-1,使得出現類情況的時候,P都能夠一次性直接滑動到next[0]=-1的位置,這相當於我們在求next函式值的時候,把這些冗餘的比較進行預處理,如此就可以消除模式串與主串之間這樣的多餘比較。

設主串中的si和pj失配,按原定義模式串中應滑動到位置k=next[j],進行si和pk的匹配,而模式串中pk滿足pk=pj,因此si不需要再和pk進行比較,而直接和pnext[k]匹配,也就是說此時的next[j]=next[k]=next[next[j]]。

i 0 1 2 3 4  
P a a a a b  
P   a a a a b
j   0 1 2 3 4
next[i] -1 0 1 2 3  
nextval[i] -1 -1 -1 -1 3  

對上面的get_next函式稍加改進得到:

void get_nextval(SString P,  int nextval[]){
    //求模式串P的next函式修正值並存入陣列nextval
    i = 0; j = -1; nextval[0] = -1;
    while(i < len(P)-1){
        if(j == -1 || P[i] == P[j]){
            ++i; ++j;
            if(P[i] != P[j]) nextval[i] = j;//
            else nextval[i] = nextval[j];//nextval[i]=j意味著當主串的當前字元與模式串中的pi不匹配時,應與模式串的pj比較。而當pj=pi時,所以應該與模式串的pnext[j]比較。
     }
    else j=nextval[j];
  }
}

 

五、KMP演算法

求得next[j]的函式值之後,我們就可以根據KMP演算法進行字串匹配了。

在匹配過程中,如果si=pj,則i和j分別增1;

否則,i不變,j移動到next[j]的位置繼續比較,以此類推,直至下面兩種可能:

1)j退至某個next值(next[next[…next[j]...]])時字元比較相等,則指標各自增1,繼續進行匹配;

2)退到next[next[…next[j]...]]為-1,此時需要將模式串向右滑動一個位置,即從主串的下一個字元si+1起和模式串p0開始重新匹配。

KMP演算法虛擬碼如下:

int Index_KMP(SString S, SString P, int pos){  
    //利用模式串P的next函式求P在主串S中第pos個字元之後的位置的KMP演算法。
    //其中,P非空,0<=pos<StrLength(S)。  
    i = pos; j = 0;  
    while(i < len(S) && j < len(P)){
        if(j == -1 || S[i] == P[j]) { ++i; ++j; }//繼續比較後繼字元
        else   j = next[j];//模式串向右滑動
    }  
    if(j == len(P))   return  i - len(P);//匹配成功  
    else   return  0;  
}//Index_KMP

 

六、直接看出next[j]函式值(這部分可以用,不過原理不太清晰)

根據以上next[j]的定義:

(1)next[j] = -1(j=0);

(2)next[j] = Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}(j!=0且集合有解);

(3)next[j] = 0(j!=0且集合無解);

可以直接看出模式串的next函式值。這主要遵循2條規則

a)如果上一個next[j]!=-1,next[j+1]=Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'};

b)如果上一個next[j]=-1,由next[j]的求解演算法知,此時主串第j位與模式串第0位對齊,比較這兩位:①如果不等,則主串的第j+1位與模式串的第0位對齊,故next[j+1]=0;②如果相等,則next[j+1]=Max{k|1<k<j且'pj-kpj-k+1...pj-1'='p0p1...pk-1'}

 

 (轉)http://www.cnblogs.com/wentfar/archive/2011/12/17/2291340.html 

參考文獻:《資料結構》,嚴蔚敏著 

 

相關文章