關於串的模式匹配,簡單來講,就是從S串中找到子串T的位置。假設有串S=“acabcabcacbab”,子串T="abcac",這個問題,首先你會怎麼做呢?(可以先做思考)
這個問題,在<數據結構>一書中描述得已經足夠清楚,本篇特在此進行梳理和回顧。
1.樸素匹配(Brute Force簡稱BF)
可能你想到的方法剛好就是下面的將要介紹的樸素匹配算法,下面我們看看它的匹配過程:
圖1
可以看出,算法是對於主串S和子串逐字符比較,一旦遇到未匹配的字符就從主串中開始匹配的下個位置再進行逐字匹配,比如,在第三趟中主串S中b和子串T中的c不匹配,
那麼就需要從主串中開始匹配的字符a(i=3)的下個位置b(i=4)再和子串逐字匹配見第四趟匹配,這樣,一直到匹配到子串T或者未找到。算法的實現也很容易理解:
int index(SString S,SString T,int pos)
{
int i,j;
if(1<=pos&&pos<=S[0]){
i=pos;
j=1;
while(i<=S[0]&&j<=T[0]){
if(S[i]==T[j]){
++i;++j;
}else{ //回溯
i=i-j+2;
j=1;
}
}
if(j>T[0])
return i-T[0];
else
return 0;
}
return 0;
}
注:SString 爲串的順序存儲結構表示 S[0]存儲串的長度,字符索引從1開始,詳見<BDS之串>一篇
2.KMP算法
KMP算法是由Knuth(D.E.Knuth)、Morris(J.H.Morris)和Pratt(V.R.Pratt)三人設計的線性時間字符串匹配算法。下面讓我們在這裏一起揭開KMP算法的奧祕之處。
KMP是一種無回溯的匹配算法,對於上面那個例子來講,每次遇到字符不匹配時,不再回溯指針i,而是利用已經得到的“部分匹配”的結果將模式向右滑動儘可能遠的一段距離之後,繼續進行比較,可能不太容易理解,那麼,我們繼續使用上圖1中的實例進行說明:
在圖1中,經過觀察,第三趟當i=7,j=5時主串S的字符b和子串T的字符c失配時,在第四、五、六趟的中i=4(b),j=1和i=5(c),j=1以及i=6(a),j=1這三次比較是不必要進行的,理由如下:
(1).從第三趟的部分匹配結果中就能得到主串中第4,5,6個字符必然爲b,c,a(即模式中的第2,3,4個字符)。
圖2
(2)因爲子串(模式串)T的第一個字符爲a,如果回溯,a就需要分別跟主串中的(b,c,a)比較,顯然b,c跟a肯定會不匹配,可以不進行,a和a比較會匹配,也可以不進行,因爲對於無回溯的方法,這裏我們關心的是的基準字符b(即主串S中i=7的字符)和模式串中哪個字符比較。可以看出,應該是模式串中i=2的字符。所以KMP算法的核心問題就是解決,當主串中i位置的字符和子串中j位置的字符不匹配時,主串i下個需要比較的子串中字符的位置如何計算?
下來我們就來探討這個問題,假設一般的情況下,假設主串S=s1s2s3......sn ,子串T=p1p2p3......pm,我們的問題是主串S中第i個字符與子串T中第j個字符失配,主串中第i個字符接下來和子串中哪個字符繼續進行比較?
假設此時主串與子串中的第K個字符進行比較,則模式中前k-1個字符必滿足下面的等式:
①
即主串第i個字符的前k-1個字符必須等於子串第k個字符的前k-1個字符
同時,從部分匹配的結果中,也可以得到下面等式
②
由①②等式可以得到:
③
等式③的意義在於,如果模式串滿足了等式③,則在匹配過程中,主串中的第i個字符和子串的第j個字符失配時,僅需將子串向右滑動至第k個字符和主串的字符i對齊,此時子串的前k-1個字符和主串第i個字符前的k-1個字符相等。
這裏,令next[j]=k,next[j]用來表示當子串第j個字符與主串相應字符失配時,在模式串中需要重新和主串中該字符進行比較的字符位置。由此引出模式串next函數的定義:
通過next函數的定義,我們可以很容易的求得對應模式串的next值,比如下面的模式串:
圖3
求得模式的next函數後,匹配可如下進行,在匹配過程中,若S[i]==T[j],則i和j分別增加1,否則,i不變,而j退到next[j]的位置再進行比較,如果相等,則指針再增加1,否則j再推到下一個next值的位置,以此類推。直至下列兩種可能:一種是j退到某個next值時字符比較相等,則指針各自加1繼續比較;另一種情況是j退到0,即從主串的下個字符S[i+1]起和模式串重新進行匹配。下面看下利用模式串的next進行匹配的實例過程,子串next值參見上圖:
圖4
基於這個思路,我們可以寫出基於next的kmp算法,它其實類似於樸素匹配算法:
int index_kmp(SString S,SString T,int pos)
{
int i,j;
if(1<=pos&&pos<=S[0]){
i=pos;
j=1;
get_next(T,next);
while(i<=S[0]&&j<=T[0]){
if(j==0 || S[i]==T[j]){
++i;
++j;
}else{
j=next[j];
}
}
if(j>T[0])
return i-T[0];
else
return 0;
}
return 0;
}
KMP算法依賴於模式串的next函數值,那麼根據next函數的定義怎麼求取模式串的next函數值呢?需要明確的是求取next值只和模式串本身相關而無關主串,我們可以分析其定義根據遞推的方式來求其值。回顧下next函數定義,我們知道next[1]=0,且如果next[j]=k,則有滿足
(其中1<k<j, 且不可能存在k'>k滿足這個等式 )
此時,next[j+1]=?,可能有兩種情況:
(1)pk=pj時,則表明在模式串中:
這意味着next[j+1]=k+1,即
next[j+1]= next[j]+1④
比如,圖3中的next[6]=next[5+1]=next[5]+1 , 因爲滿足next[5]=2 且 p2=p5;
(2)pk≠pj時,則表明在模式串中:
這時,我們可以將整個模式串看做一個模式匹配的問題,整個模式串既是主串又是模式串,且以滿足p[j-k+1]=p[1] ,p[j-k+2]=p2,.....,p[j-1]=p[k-1],當p[j]≠p[k]時應將模式向右滑動至以模式中的第next[k]個字符和主串中的第j個字符相比較。若next[k]=k',則p[j]=p[k'],則說明在主串中第j+1個字符之前存在一個長度爲k'的最長子串,和模式串中首字符其長度爲k‘的子串相等,即
⑤
也就是說,next[j+1]=k'+1 即
next[j+1]=next[k]+1 ⑥
同理,若p[j]≠p[k'],則將模式串繼續向右滑動直至將模式中的第next[k']個字符和主串中的p[j]對齊......,依次類推,直至p[j]和模式中某個字符匹配成功或者或者不存在任何k'滿足⑤等式,則
next[j+1]=1 ⑦
或許,根據這些推導式可能還不能夠清楚,那我們再看看圖3中的模式串。求next[7]時類似於第二種情況,此時next[6]=3(j=6,k=3),且p[6]≠p[3],按照上面描述的, 將其看做一個模式匹配問題,那麼在失配時,應將模式串滑動至第next[k]=k'個字符於當前字符p[j]比較這裏next[3]=1,p[j]=p[6],這裏,由於p[6]≠p[1],則繼續將模式串右滑至第next[1](即next[k'])=0個字符和p[j]對齊,顯然這裏不存在k'滿足⑤等式,則根據next函數的定義 ,有 next[j+1]=1,即next[7]=1.
這裏假設我們將模式串由原來的T=“abaabcac”調整爲T‘="abcabaac",將可以看到另外一種情況:
1 2 3 4 5 6 7 8 ---序號
a b c a b a a c ---T‘
0 1 1 1 2 3 2 2 ---next值
同樣,對於求next[7]的情況,此時next[6]=3(j=6,k=3),且p[6]≠p[3],將模式串滑動至next[3]=1(k'=1),此時滿足p[j]=p[k'],即p[6]=p[1],則根據公式⑥可以得到next[7]=next[3]+1=2.
到此,我們討論了求解模式串next值的所有情況,下面我們根據上面的公式實現求解next值的算法:
void get_next(SString T,int next[])
{
int i = 1,j=0;
next[1]=0;
while(i<T[0]){
if(j==0 || T[i]==T[j]){
++i;
++j;
next[i]=next[j];//滿足公式4,6,7
}else{
j=next[j];//向右滑動next[k]=k'
}
}
}