原創文章,轉載請註明出處:http://blog.csdn.net/fastsort/article/details/9903153
1、字串匹配
所謂字串匹配,就是在主串S中尋找模式串P,如果存在,則返回P在S中的起始下標,否則返回不存在資訊(這裡用-1表示)。例如:
S=abcdabce,T=cda,
則返回2,如果T=cdd,則返回-1.
考慮C/C++中字串下標都是從0開始,以下討論都是在字串下標從0開始的前提下進行【有特殊說明的除外】。
2、常規演算法
在確定找到高效演算法前,“暴力”演算法(brute force)雖然效率差強人意,但是總是值得信任的。
字串匹配演算法也是這樣。直接匹配也很簡單,把主串S的第一個字元和模式串P的第一個字元對齊,然後兩個串同時向後比較:
如果一直到P的結尾都匹配,很好,匹配成功,返回0;
如果中間存在不匹配的字元,那麼把S的第二個字元和P的第一個字元對齊,然後兩個串同時向後比較,直到繼續S的第三個字元或者是匹配成功返回1……
重複上述過程,直到S的結尾,這時返回-1,說明匹配失敗。
代碼如下:
int match_1(const char * s, const char *p){ int slen=strlen(s),plen=strlen(p); int i=0,j=0; while(i<slen) { while(j<plen &&(i+j)<slen && s[i+j]==p[j]) j++; if(j==plen) return i; i++; j=0; } return -1;}
這段代碼運行良好,但是為了對比,將其改為如下形式:
int match_2(const char * s, const char *p){ int slen=strlen(s),plen=strlen(p); int i=0,j=0; while(i<slen && j<plen) { if(s[i]==p[j])///匹配 { i++; j++; } else///不匹配了 { i = i-j+1;///回退S的指標 j = 0;///P從頭開始 } } if(j==plen) return i-j;///匹配成功 else return -1;}
簡單的說,就是i,j初始時分別指向s和p的起始位置,匹配時i,j同時向後移動,失配時i指標回退,j指標置零。
對於以上代碼,你應當完全理解,給你筆和紙你就能準確無誤的立刻寫出來。這樣你就對其工作過程了如指掌,否則後面的內容將難於理解。
複雜度分析:在最壞情況下,p每次都匹配到倒數第二個字元,然後最後一個字元不匹配,這時從頭再來,顯然每次p匹配都需要O(m)(其中m=strlen(p)),對s中的每個字元都匹配一遍,也需要O(n)(其中n=strlen(s)),總的複雜度就是O(mn)。
例如s=0000000000001,p=00001時,m=strlen(p)=5,n=strlen(s)=13.對於i∈[0-12],j∈[0-4],每次匹配到p[4]時,發現s[i]!=p[j]不匹配,都需要從頭開始(j=0,i=i-j+1)下次匹配,即所謂的“回溯”。
我們發現,在這個過程中其實有很多回溯過程是沒有必要的。比如:
i 0123456
s 0000000000001
p 00001
j 01234
在發現s[4]!=p[4]時,下次比較將從s[1]和p[0]比較開始:
i 0123456
s 0000000000001
p 00001
j 01234
一直比較到s[5]!=p[4]。其實如果注意到,在上一輪比較中,s[4]!=p[4]時(i=4,j=4),已經有
s[0...3]=p[0...3]
①
所以有
s[1...3]=p[1...3]
②
而
p[1...3]=p[0...2]
③
③中,p[0...2]為p的首碼,p[1...3]為尾碼,這是由p本身的性質決定的,由②和③可以得到
s[1...3]=p[0...2]
④
看到這裡你有什麼感覺?既然s[1...3]=p[0...2]了,那為什麼下一輪匹配過程還要從s[1]和p[0]開始呢?!直接比較s[4]和p[3](即s[i]和p[k],k=3)就可以了。kmp演算法就是基於這個原理的,即發生失配後不需要每次都從p串的起始位置開始新的一輪比較(即i指標回退、j指標置零)。
上面這段分析,尤其是這三個等式,最好理解透徹。這裡沒有用各種ijk,而是用實際的字串和數字表示,就是為了便於理解。理解透徹之後,就開始正式講解kmp演算法了。
3、kmp演算法
kmp演算法對一般字元串匹配演算法(即match_2)的改進之處在於:發生失配之後,i指標不動,j指標指向合適的位置,開始下一輪的匹配。
假設合適的位置已經儲存在一個數組next[n]中,n=strlen(p)。那麼kmp的代碼就是:
int match_3(const char * s, const char *p)
{
int slen=strlen(s),plen=strlen(p);
int i=0,j=0;
while(i<slen && j<plen)
{
if(s[i]==p[j])///匹配
{
i++;
j++;
}
else///不匹配了
{
//i =i-j+1; i指標不變,不再回退
j = next[j];///j指標從某個位置開始
}
}
if(j==plen) return i-j;///匹配成功
else return -1;
}
發生改變的地方就在else裡:i指標不再回退,j指標不是歸零而是某個特定的值。看上去是不是比樸素匹配演算法更簡單?貌似是的,但是問題是這個特定值是什嗎?也就是說這個next數組怎麼求呢?好,現在來說這個next數組。
從第二部分的最後面的分析(就是①②③那三個式子那)我們知道,當s[i]!=p[j]時(失配了),已經匹配的部分為
s[i-j…i-1]=p[0…j-1] ④
即s[i]和p[j]的前j-1個字元。
如果想從p的第k個字元開始下一輪的匹配而不是回溯,那麼必須要滿足:p的前k-1個字元和s[i]前面的k-1個字元匹配(即k=next[j])。
p的前k-1個字元為: p[0…k-1]
s[i]的前k-1個字元為:s[i-k…i-1]
也就是
p[0…k-1]=s[i-k…i-1] ⑤
由④可得:
s[i-k…i-1]=p[j-k…j-1] ⑥
即s[i-j…i-1]和後k-1個字元和p的後k-1個字元相等。
由⑤⑥得
p[0…k-1]=p[j-k…j-1] ⑦
如果第一次看到這裡,可能就有點暈了。我們再回顧下我們的問題。在p[j]處發生失配(s[i]!=p[j])後,i不動,j不需要置零,而是從k=next[j]處取得,那麼這個k要滿足⑦。
⑦是什麼意思呢?左面是p的前k個字元(首碼),右面是p[0…j-1]的後k個字元(尾碼)。這個k越大越好,為什嗎?因為越大j向後滑動的越多,效率越高。但是必須有k<=j-1 => k<j.
為便於理解,舉個例子,對於p=”000011”:
j=0 ?
j=1 p[1]前有p[0]=p[0], next[1]=0
j=2 p[0-1]=p[0-1], next[2]=1
j=3 p[0-2]=p[0-2], next[3]=2
j=4 p[0-3]=p[0-3], next[4]=3
j=5 p[0…4]沒有首碼=尾碼, next[5]=0
j=0時怎麼辦呢?考慮最原始的情況,s[i]!=p[0]了,第一個就不匹配,當然沒法跳了,開始下一輪即可。所以這裡可以設定一個特殊值,表示第一個(j=0)就不匹配,一般設定為-1,可以簡化程式。在j=-1時,需要進行下一輪匹配(i++;j++)j++後剛好是0。如果你一定要設定為-9,那麼你要判斷,當j==-9時,下一輪要:(i++; j=0;)。你還會注意到,對任何長度大於2的p串,next[1]==0是恒成立的。
再來一個例子:
p[]=”abababacd”的next數組next[9]:
j=0,k=next[0]=-1;
j=1,k=next[1]=0
j=2,p[2]之前有”ab”,其首碼只有’a’!=尾碼’b’,所以k=next[2]=0
j=3,p[3]前為”aba”,p[0]=p[2], k=next[3]=1
j=4,p[4]前為”abab”,p[0…1]=p[2…3], k=next[4]=2
j=5,p[5]前為ababa,p[0…2]=p[2…4], k=next[5]=3
j=6,p[6]前為ababab,p[0…3]=p[2…5], k=next[6]=4
j=7,p[7]前為abababa,p[0…4]=p[2…6],k=next[7]=5
j=8,p[8]前為abababac,由於最後一個字母是c,任何首碼都沒有和其相等的尾碼,所以k=next[8]=0
相信通過這2個例子,你應該理解了next數組了吧。
由於next數組中存在j=-1的情況,所以我們的函數應該做少許修改:
intkmp(const char * s, const char *p){ int slen=strlen(s),plen=strlen(p); int *next = (int*)malloc(sizeof(int)*plen); getNext(p,next); int i=0,j=0; while(i<slen && j<plen) { if(j==-1|| s[i]==p[j])///j=-1時,也是進行下一輪匹配 { i++; j++; } else { j = next[j];///i,j不再回溯 } } free(next); if(j==plen) return i-j; else return -1;}
我把函數名也修改為kmp,這也是kmp演算法的最終形式(未最佳化的,後面再說最佳化的情況)。總共不到10行。
其中的getNext()函數就是計算next數組的,如果你理解了next數組的計算方法,你可以動手寫一下試試,其代碼也不過十行,而且和上面的kmp函數驚人的相似。至於getNext()的思路和具體代碼,還在留給下一篇blog吧。
原創文章,轉載請註明出處:http://blog.csdn.net/fastsort/article/details/9903153