來自CSDN A_B_C_ABC 網友ios
KMP字符串模式匹配通俗點說就是一種在一個字符串中定位另外一個串的高效算法。簡單匹配算法的時間複雜度爲O(m*n);KMP匹配算法。能夠證實它的時間複雜度爲O(m+n).。c++
一. 簡單匹配算法算法
先來看一個簡單匹配算法的函數:編程
int Index_BF ( char S [ ], char T [ ], int pos )數組
{編程語言
/* 若串 S 中從第pos(S 的下標0≤pos<StrLength(S))個字符編輯器
起存在和串 T 相同的子串,則稱匹配成功,返回第一個函數
這樣的子串在串 S 中的下標,不然返回 -1 */spa
int i = pos, j = 0;.net
while ( S[i+j] != '\0'&& T[j] != '\0')
if ( S[i+j] == T[j] )
j ++; // 繼續比較後一字符
else
{
i ++; j = 0; // 從新開始新的一輪匹配
}
if ( T[j] == '\0')
return i; // 匹配成功 返回下標
else
return -1; // 串S中(第pos個字符起)不存在和串T相同的子串
} // Index_BF
此算法的思想是直截了當的:將主串S中某個位置i起始的子串和模式串T相比較。即從 j=0 起比較 S[i+j] 與 T[j],若相等,則在主串 S 中存在以 i 爲起始位置匹配成功的可能性,繼續日後比較( j逐步增1 ),直至與T串中最後一個字符相等爲止,不然改從S串的下一個字符起從新開始進行下一輪的"匹配",即將串T向後滑動一位,即 i 增1,而 j 退回至0,從新開始新一輪的匹配。
例如:在串S=」abcabcabdabba」中查找T=」 abcabd」(咱們能夠假設從下標0開始):先是比較S[0]和T[0]是否相等,而後比較S[1] 和T[1]是否相等…咱們發現一直比較到S[5] 和T[5]纔不等。如圖:
當這樣一個失配發生時,T下標必須回溯到開始,S下標回溯的長度與T相同,而後S下標增1,而後再次比較。如圖:
此次馬上發生了失配,T下標又回溯到開始,S下標增1,而後再次比較。如圖:
此次馬上發生了失配,T下標又回溯到開始,S下標增1,而後再次比較。如圖:
又一次發生了失配,因此T下標又回溯到開始,S下標增1,而後再次比較。此次T中的全部字符都和S中相應的字符匹配了。函數返回T在S中的起始下標3。如圖:
二. KMP匹配算法
仍是相同的例子,在S=」abcabcabdabba」中查找T=」abcabd」,若是使用KMP匹配算法,當第一次搜索到S[5] 和T[5]不等後,S下標不是回溯到1,T下標也不是回溯到開始,而是根據T中T[5]==’d’的模式函數值(next[5]=2,爲何?後面講),直接比較S[5] 和T[2]是否相等,由於相等,S和T的下標同時增長;由於又相等,S和T的下標又同時增長。。。最終在S中找到了T。如圖:
KMP匹配算法和簡單匹配算法效率比較,一個極端的例子是:
在S=「AAAAAA…AAB「(100個A)中查找T=」AAAAAAAAAB」, 簡單匹配算法每次都是比較到T的結尾,發現字符不一樣,而後T的下標回溯到開始,S的下標也要回溯相同長度後增1,繼續比較。若是使用KMP匹配算法,就沒必要回溯.
對於通常文稿中串的匹配,簡單匹配算法的時間複雜度可降爲O (m+n),所以在多數的實際應用場合下被應用。
KMP算法的核心思想是利用已經獲得的部分匹配信息來進行後面的匹配過程。看前面的例子。爲何T[5]==’d’的模式函數值等於2(next[5]=2),其實這個2表示T[5]==’d’的前面有2個字符和開始的兩個字符相同,且T[5]==’d’不等於開始的兩個字符以後的第三個字符(T[2]=’c’).如圖:
也就是說,若是開始的兩個字符以後的第三個字符也爲’d’,那麼,儘管T[5]==’d’的前面有2個字符和開始的兩個字符相同,T[5]==’d’的模式函數值也不爲2,而是爲0。
前面我說:在S=」abcabcabdabba」中查找T=」abcabd」,若是使用KMP匹配算法,當第一次搜索到S[5] 和T[5]不等後,S下標不是回溯到1,T下標也不是回溯到開始,而是根據T中T[5]==’d’的模式函數值,直接比較S[5] 和T[2]是否相等。。。爲何能夠這樣?
剛纔我又說:「(next[5]=2),其實這個2表示T[5]==’d’的前面有2個字符和開始的兩個字符相同」。請看圖 :由於,S[4] ==T[4],S[3] ==T[3],根據next[5]=2,有T[3]==T[0],T[4] ==T[1],因此S[3]==T[0],S[4] ==T[1](兩對至關於間接比較過了),所以,接下來比較S[5] 和T[2]是否相等。。。
有人可能會問:S[3]和T[0],S[4] 和T[1]是根據next[5]=2間接比較相等,那S[1]和T[0],S[2] 和T[0]之間又是怎麼跳過,能夠不比較呢?由於S[0]=T[0],S[1]=T[1],S[2]=T[2],而T[0] != T[1], T[1] != T[2],==> S[0] != S[1],S[1] != S[2],因此S[1] != T[0],S[2] != T[0]. 仍是從理論上間接比較了。
有人疑問又來了,你分析的是否是特殊輕況啊。
假設S不變,在S中搜索T=「abaabd」呢?答:這種狀況,當比較到S[2]和T[2]時,發現不等,就去看next[2]的值,next[2]=-1,意思是S[2]已經和T[0] 間接比較過了,不相等,接下來去比較S[3]和T[0]吧。
假設S不變,在S中搜索T=「abbabd」呢?答:這種狀況當比較到S[2]和T[2]時,發現不等,就去看next[2]的值,next[2]=0,意思是S[2]已經和T[2]比較過了,不相等,接下來去比較S[2]和T[0]吧。
假設S=」abaabcabdabba」在S中搜索T=「abaabd」呢?答:這種狀況當比較到S[5]和T[5]時,發現不等,就去看next[5]的值,next[5]=2,意思是前面的比較過了,其中,S[5]的前面有兩個字符和T的開始兩個相等,接下來去比較S[5]和T[2]吧。
總之,有了串的next值,一切搞定。那麼,怎麼求串的模式函數值next[n]呢?(本文中next值、模式函數值、模式值是一個意思。)
三. 怎麼求串的模式值next[n]
定義:
(1)next[0]= -1 意義:任何串的第一個字符的模式值規定爲-1。
(2)next[j]= -1 意義:模式串T中下標爲j的字符,若是與首字符
相同,且j的前面的1—k個字符與開頭的1—k
個字符不等(或者相等但T[k]==T[j])(1≤k<j)。
如:T=」abCabCad」 則 next[6]=-1,因T[3]=T[6]
(3)next[j]=k 意義:模式串T中下標爲j的字符,若是j的前面k個
字符與開頭的k個字符相等,且T[j] != T[k] (1≤k<j)。
即T[0]T[1]T[2]。。。T[k-1]==
T[j-k]T[j-k+1]T[j-k+2]…T[j-1]
且T[j] != T[k].(1≤k<j);
(4) next[j]=0 意義:除(1)(2)(3)的其餘狀況。
舉例:
01)求T=「abcac」的模式函數的值。
next[0]= -1 根據(1)
next[1]=0 根據 (4) 因(3)有1<=k<j;不能說,j=1,T[j-1]==T[0]
next[2]=0 根據 (4) 因(3)有1<=k<j;(T[0]=a)!=(T[1]=b)
next[3]= -1 根據 (2)
next[4]=1 根據 (3) T[0]=T[3] 且 T[1]=T[4]
即
下標 |
0 |
1 |
2 |
3 |
4 |
T |
a |
b |
c |
a |
c |
next |
-1 |
0 |
0 |
-1 |
1 |
若T=「abcab」將是這樣:
下標 |
0 |
1 |
2 |
3 |
4 |
T |
a |
b |
c |
a |
b |
next |
-1 |
0 |
0 |
-1 |
0 |
爲何T[0]==T[3],還會有next[4]=0呢, 由於T[1]==T[4], 根據 (3)」 且T[j] != T[k]」被劃入(4)。
02)來個複雜點的,求T=」ababcaabc」 的模式函數的值。
next[0]= -1 根據(1)
next[1]=0 根據(4)
next[2]=-1 根據 (2)
next[3]=0 根據 (3) 雖T[0]=T[2] 但T[1]=T[3] 被劃入(4)
next[4]=2 根據 (3) T[0]T[1]=T[2]T[3] 且T[2] !=T[4]
next[5]=-1 根據 (2)
next[6]=1 根據 (3) T[0]=T[5] 且T[1]!=T[6]
next[7]=0 根據 (3) 雖T[0]=T[6] 但T[1]=T[7] 被劃入(4)
next[8]=2 根據 (3) T[0]T[1]=T[6]T[7] 且T[2] !=T[8]
即
下標 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
T |
a |
b |
a |
b |
c |
a |
a |
b |
c |
next |
-1 |
0 |
-1 |
0 |
2 |
-1 |
1 |
0 |
2 |
只要理解了next[3]=0,而不是=1,next[6]=1,而不是= -1,next[8]=2,而不是= 0,其餘的好象都容易理解。
03) 來個特殊的,求 T=」abCabCad」 的模式函數的值。
下標 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
T |
a |
b |
C |
a |
b |
C |
a |
d |
next |
-1 |
0 |
0 |
-1 |
0 |
0 |
-1 |
4 |
next[5]= 0 根據 (3) 雖T[0]T[1]=T[3]T[4],但T[2]==T[5]
next[6]= -1 根據 (2) 雖前面有abC=abC,但T[3]==T[6]
next[7]=4 根據 (3) 前面有abCa=abCa,且 T[4]!=T[7]
若T[4]==T[7],即T=」 adCadCad」,那麼將是這樣:next[7]=0, 而不是= 4,由於T[4]==T[7].
下標 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
T |
a |
d |
C |
a |
d |
C |
a |
d |
next |
-1 |
0 |
0 |
-1 |
0 |
0 |
-1 |
0 |
若是你以爲有點懂了,那麼
練習:求T=」AAAAAAAAAAB」 的模式函數值,並用後面的求模式函數值函數驗證。
意義:
next 函數值到底是什麼含義,前面說過一些,這裏總結。
設在字符串S中查找模式串T,若S[m]!=T[n],那麼,取T[n]的模式函數值next[n],
1. next[n]= -1 表示S[m]和T[0]間接比較過了,不相等,下一次比較 S[m+1] 和T[0]
2. next[n]=0 表示比較過程當中產生了不相等,下一次比較 S[m] 和T[0]。
3. next[n]= k >0 但k<n, 表示,S[m]的前k個字符與T中的開始k個字符已經間接比較相等了,下一次比較S[m]和T[k]相等嗎?
4. 其餘值,不可能。
四. 求串T的模式值next[n]的函數
說了這麼多,是否是以爲求串T的模式值next[n]很複雜呢?要叫我寫個函數出來,目前來講,我寧願去登天。好在有現成的函數,當初發明KMP算法,寫出這個函數的先輩,令我佩服得六體投地。我等後生小子,理解起來,都要反覆琢磨。下面是這個函數:
void get_nextval(const char *T, int next[])
{
// 求模式串T的next函數值並存入數組 next。
int j = 0, k = -1;
next[0] = -1;
while ( T[j/*+1*/] != '\0' )
{
if (k == -1 || T[j] == T[k])
{
++j; ++k;
if (T[j]!=T[k])
next[j] = k;
else
next[j] = next[k];
}// if
else
k = next[k];
}// while
////這裏是我加的顯示部分
// for(int i=0;i<j;i++)
//{
// cout<<next[i];
//}
//cout<<endl;
}// get_nextval
另外一種寫法,也差很少。
void getNext(const char* pattern,int next[])
{
next[0]= -1;
int k=-1,j=0;
while(pattern[j] != '\0')
{
if(k!= -1 && pattern[k]!= pattern[j] )
k=next[k];
++j;++k;
if(pattern[k]== pattern[j])
next[j]=next[k];
else
next[j]=k;
}
////這裏是我加的顯示部分
// for(int i=0;i<j;i++)
//{
// cout<<next[i];
//}
//cout<<endl;
}
下面是KMP模式匹配程序,各位能夠用他驗證。記得加入上面的函數
#include <iostream.h>
#include <string.h>
int KMP(const char *Text,const char* Pattern) //const 表示函數內部不會改變這個參數的值。
{
if( !Text||!Pattern|| Pattern[0]=='\0' || Text[0]=='\0' )//
return -1;//空指針或空串,返回-1。
int len=0;
const char * c=Pattern;
while(*c++!='\0')//移動指針比移動下標快。
{
++len;//字符串長度。
}
int *next=new int[len+1];
get_nextval(Pattern,next);//求Pattern的next函數值
int index=0,i=0,j=0;
while(Text[i]!='\0' && Pattern[j]!='\0' )
{
if(Text[i]== Pattern[j])
{
++i;// 繼續比較後繼字符
++j;
}
else
{
index += j-next[j];
if(next[j]!=-1)
j=next[j];// 模式串向右移動
else
{
j=0;
++i;
}
}
}//while
delete []next;
if(Pattern[j]=='\0')
return index;// 匹配成功
else
return -1;
}
int main()//abCabCad
{
char* text="bababCabCadcaabcaababcbaaaabaaacababcaabc";
char*pattern="adCadCad";
//getNext(pattern,n);
//get_nextval(pattern,n);
cout<<KMP(text,pattern)<<endl;
return 0;
}
五.其餘表示模式值的方法
上面那種串的模式值表示方法是最優秀的表示方法,從串的模式值咱們能夠獲得不少信息,如下稱爲第一種表示方法。第二種表示方法,雖然也定義next[0]= -1,但後面毫不會出現 -1,除了next[0],其餘模式值next[j]=k(0≤k<j)的意義能夠簡單當作是:下標爲j的字符的前面最多k個字符與開始的k個字符相同,這裏並不要求T[j] != T[k]。其實next[0]也能夠定義爲0(後面給出的求串的模式值的函數和串的模式匹配的函數,是next[0]=0的),這樣,next[j]=k(0≤k<j)的意義均可以簡單當作是:下標爲j的字符的前面最多k個字符與開始的k個字符相同。第三種表示方法是第一種表示方法的變形,即按第一種方法獲得的模式值,每一個值分別加1,就獲得第三種表示方法。第三種表示方法,我是從論壇上看到的,沒看到詳細解釋,我估計是爲那些這樣的編程語言準備的:數組的下標從1開始而不是0。
下面給出幾種方法的例子:
表一。
下標 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
T |
a |
b |
a |
b |
c |
a |
a |
b |
c |
(1) next |
-1 |
0 |
-1 |
0 |
2 |
-1 |
1 |
0 |
2 |
(2) next |
-1 |
0 |
0 |
1 |
2 |
0 |
1 |
1 |
2 |
(3) next |
0 |
1 |
0 |
1 |
3 |
0 |
2 |
1 |
3 |
第三種表示方法,在我看來,意義不是那麼明瞭,再也不討論。
表二。
下標 |
0 |
1 |
2 |
3 |
4 |
T |
a |
b |
c |
a |
c |
(1)next |
-1 |
0 |
0 |
-1 |
1 |
(2)next |
-1 |
0 |
0 |
0 |
1 |
表三。
下標 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
T |
a |
d |
C |
a |
d |
C |
a |
d |
(1)next |
-1 |
0 |
0 |
-1 |
0 |
0 |
-1 |
0 |
(2)next |
-1 |
0 |
0 |
0 |
1 |
2 |
3 |
4 |
對比串的模式值第一種表示方法和第二種表示方法,看錶一:
第一種表示方法next[2]= -1,表示T[2]=T[0],且T[2-1] !=T[0]
第二種表示方法next[2]= 0,表示T[2-1] !=T[0],但並無論T[0] 和T[2]相不相等。
第一種表示方法next[3]= 0,表示雖然T[2]=T[0],但T[1] ==T[3]
第二種表示方法next[3]= 1,表示T[2] =T[0],他並無論T[1] 和T[3]相不相等。
第一種表示方法next[5]= -1,表示T[5]=T[0],且T[4] !=T[0],T[3]T[4] !=T[0]T[1],T[2]T[3]T[4] !=T[0]T[1]T[2]
第二種表示方法next[5]= 0,表示T[4] !=T[0],T[3]T[4] !=T[0]T[1] ,T[2]T[3]T[4] !=T[0]T[1]T[2],但並無論T[0] 和T[5]相不相等。換句話說:就算T[5]==’x’,或T[5]==’y’,T[5]==’9’,也有next[5]= 0 。
從這裏咱們能夠看到:串的模式值第一種表示方法能表示更多的信息,第二種表示方法更單純,不容易搞錯。固然,用第一種表示方法寫出的模式匹配函數效率更高。好比說,在串S=「adCadCBdadCadCad 9876543」中匹配串T=「adCadCad」, 用第一種表示方法寫出的模式匹配函數,當比較到S[6] != T[6] 時,取next[6]= -1(表三),它能夠表示這樣許多信息: S[3]S[4]S[5]==T[3]T[4]T[5]==T[0]T[1]T[2],而S[6] != T[6],T[6]==T[3]==T[0],因此S[6] != T[0],接下來比較S[7]和T[0]吧。若是用第二種表示方法寫出的模式匹配函數,當比較到S[6] != T[6] 時,取next[6]= 3(表三),它只能表示:S[3]S[4]S[5]== T[3]T[4]T[5]==T[0]T[1]T[2],但不能肯定T[6]與T[3]相不相等,因此,接下來比較S[6]和T[3];又不相等,取next[3]= 0,它表示S[3]S[4]S[5]== T[0]T[1]T[2],但不會肯定T[3]與T[0]相不相等,即S[6]和T[0] 相不相等,因此接下來比較S[6]和T[0],肯定它們不相等,而後纔會比較S[7]和T[0]。是否是比用第一種表示方法寫出的模式匹配函數多繞了幾個彎。
爲何,在講明第一種表示方法後,還要講沒有第一種表示方法好的第二種表示方法?緣由是:最開始,我看嚴蔚敏的一個講座,她給出的模式值表示方法是我這裏的第二種表示方法,如圖:
她說:「next 函數值的含義是:當出現S[i] !=T[j]時,下一次的比較應該在S[i]和T[next[j]] 之間進行。」雖簡潔,但不明瞭,反覆幾遍也沒明白爲何。而她給出的算法求出的模式值是我這裏說的第一種表示方法next值,就是前面的get_nextval()函數。匹配算法也是有瑕疵的。因而我在這裏發帖說她錯了:
http://community.csdn.net/Expert/topic/4413/4413398.xml?temp=.2027246
如今看來,她沒有錯,不過有張冠李戴之嫌。我不知道,是否有人第一次學到這裏,不參考其餘資料和明白人講解的狀況下,就能搞懂這個算法(個人意思是不只是算法的大體思想,而是爲何定義和例子中next[j]=k(0≤k<j),而算法中next[j]=k(-1≤k<j))。憑良心說:光看這個講座,我就對這個教受十分敬佩,不只講課講得好,聲音悅耳,並且這門課講得井井有條,恰到好處。在KMP這個問題上出了點小差錯,多是編書的時候,在這本書上抄下了例子,在那本書上抄下了算法,結果不怎麼對得上號。由於我沒找到原書,而據有的網友說,書上已不是這樣,也許吧。提及來,教授們研究的問題比這個高深不知多少倍,哪有時間推演這個小算法呢。總之,瑕不掩玉。
書歸正傳,下面給出我寫的求第二種表示方法表示的模式值的函數,爲了從S的任何位置開始匹配T,「當出現S[i] !=T[j]時,下一次的比較應該在S[i]和T[next[j]] 之間進行。」 定義next[0]=0 。
void myget_nextval(const char *T, int next[])
{
// 求模式串T的next函數值(第二種表示方法)並存入數組 next。
int j = 1, k = 0;
next[0] = 0;
while ( T[j] != '\0' )
{
if(T[j] == T[k])
{
next[j] = k;
++j; ++k;
}
else if(T[j] != T[0])
{
next[j] = k;
++j;
k=0;
}
else
{
next[j] = k;
++j;
k=1;
}
}//while
for(int i=0;i<j;i++)
{
cout<<next[i];
}
cout<<endl;
}// myget_nextval
下面是模式值使用第二種表示方法的匹配函數(next[0]=0)
int my_KMP(char *S, char *T, int pos)
{
int i = pos, j = 0;//pos(S 的下標0≤pos<StrLength(S))
while ( S[i] != '\0' && T[j] != '\0' )
{
if (S[i] == T[j] )
{
++i;
++j; // 繼續比較後繼字符
}
else // a b a b c a a b c
// 0 0 0 1 2 0 1 1 2
{ //-1 0 -1 0 2 -1 1 0 2
i++;
j = next[j]; /*當出現S[i] !=T[j]時,
下一次的比較應該在S[i]和T[next[j]] 之間進行。要求next[0]=0。
在這兩個簡單示範函數間使用全局數組next[]傳值。*/
}
}//while
if ( T[j] == '\0' )
return (i-j); // 匹配成功
else
return -1;
} // my_KMP
六.後話--KMP的歷史
[這段話是抄的]
Cook於1970年證實的一個理論獲得,任何一個可使用被稱爲下推自動機的計算機抽象模型來解決的問題,也可使用一個實際的計算機(更精確的說,使用一個隨機存取機)在與問題規模對應的時間內解決。特別地,這個理論暗示存在着一個算法能夠在大約m+n的時間內解決模式匹配問題,這裏m和n分別是存儲文本和模式串數組的最大索引。Knuth 和Pratt努力地重建了 Cook的證實,由此建立了這個模式匹配算法。大概是同一時間,Morris在考慮設計一個文本編輯器的實際問題的過程當中建立了差很少是一樣的算法。這裏能夠看到並非全部的算法都是「靈光一現」中被發現的,而理論化的計算機科學確實在一些時候會應用到實際的應用中。