KMP字串匹配演算法理解(轉)
主串(被掃描的串):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
參考文獻:《資料結構》,嚴蔚敏著
相關文章
- 匹配字串之——KMP演算法深入理解字串KMP演算法
- KMP字串匹配演算法KMP字串匹配演算法
- 字串匹配演算法:KMP字串匹配演算法KMP
- 【字串匹配】KMP字串匹配KMP
- 字串匹配問題——KMP演算法字串匹配KMP演算法
- 字串匹配之KMP《演算法很美》字串匹配KMP演算法
- 字串匹配基礎下——KMP 演算法字串匹配KMP演算法
- kmp字串匹配,A星尋路演算法KMP字串匹配演算法
- 字串匹配演算法(三)-KMP演算法字串匹配演算法KMP
- 字串匹配-BF演算法和KMP演算法字串匹配演算法KMP
- 快速字串匹配一: 看毛片演算法(KMP)字串匹配演算法KMP
- 神奇的字串匹配:擴充套件KMP演算法字串匹配套件KMP演算法
- 第五章 字串專題 ---------------- 字串匹配(二)----KMP演算法字串匹配KMP演算法
- KMP字串模式匹配詳解KMP字串模式
- 字串匹配演算法之 BF 和 KMP 講解字串匹配演算法KMP
- KMP字串匹配學習筆記KMP字串匹配筆記
- 圖解KMP字串匹配演算法+程式碼實現圖解KMP字串匹配演算法
- KMP模式匹配演算法KMP模式演算法
- 【leetcode】28. Implement strStr() 字串匹配KMP BMLeetCode字串匹配KMP
- 模式匹配kmp演算法(c++)模式KMP演算法C++
- 【資料結構與演算法】字串匹配(Rabin-Karp 演算法和KMP 演算法)資料結構演算法字串匹配KMP
- 字串演算法--$\mathcal{KMP,Trie}$樹字串演算法KMP
- Python 細聊從暴力(BF)字串匹配演算法到 KMP 演算法之間的精妙變化Python字串匹配演算法KMP
- 字串匹配演算法(一)字串匹配演算法
- LeetCode_0028. 找出字串第一個匹配項的下標,KMP演算法的實現LeetCode字串KMP演算法
- KMP-字串KMP字串
- [譯] Swift 演算法學院 - KMP 字串搜尋演算法Swift演算法KMP字串
- 單模式匹配 KMP 演算法 簡易版學習筆記模式KMP演算法筆記
- 字串匹配演算法【未完待續】字串匹配演算法
- KMP演算法next陣列的深入理解KMP演算法陣列
- 字串匹配字串匹配
- 【資料結構與演算法】字串匹配資料結構演算法字串匹配
- 藍橋杯演算法提高——字串匹配(Java)演算法字串匹配Java
- 演算法之字串——正規表示式匹配演算法字串
- 七分鐘理解什麼是 KMP 演算法KMP演算法
- 字串匹配演算法(二)-BM演算法詳解字串匹配演算法
- 程式碼隨想錄演算法訓練營第9天 | 字串(KMP演算法) 28. 找出字串中第一個匹配項的下標 459.重複的子字串演算法字串KMP
- KMP 演算法KMP演算法
- KMP演算法KMP演算法