KMP字符串模式匹配通俗點說就是一種在一個字符串中定位另一個串的高效算法。簡單匹配算法的時間複雜度爲O(m*n);KMP匹配算法。可以證明它的時間複雜度爲O(m+n).。
一. 簡單匹配算法
先來看一個簡單匹配算法的函數:
int Index_BF ( char S [ ], char T [ ], int pos )
{
/* 若串 S 中從第pos(S 的下標0≤pos<StrLength(S))個字符
起存在和串 T 相同的子串,則稱匹配成功,返回第一個
這樣的子串在串 S 中的下標,否則返回 -1 */
int i = pos, j = 0;
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中相應的字符匹配了。函數返回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;
}