<轉>KMP字符串模式匹配詳解

我的以爲這篇文章是網上的介紹有關KMP算法更讓人容易理解的文章了,確實說得很「詳細」,耐心地把它看完確定會有所收穫的~~,另外有關模式函數值next[i]確實有不少版本啊,在另一些面向對象的算法描述書中也有失效函數 f(j)的說法,實際上是一個意思,即next[j]=f(j-1)+1,不過仍是next[j]這種表示法好理解啊:

KMP字符串模式匹配詳解ios

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 下標增 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() 函數。匹配算法也是有瑕疵的。因而我在這裏發帖說她錯了:
如今看來,她沒有錯,不過有張冠李戴之嫌。我不知道,是否有人第一次學到這裏,不參考其餘資料和明白人講解的狀況下,就能搞懂這個算法(個人意思是不只是算法的大體思想,而是爲何定義和例子中 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
v oid 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 在考慮設計一個文本編輯器的實際問題的過程當中建立了差很少是一樣的算法。這裏能夠看到並非全部的算法都是「靈光一現」中被發現的,而理論化的計算機科學確實在一些時候會應用到實際的應用中。
相關文章
相關標籤/搜索