KMP詳解
既然你已經找到這兒了,說明你已經多多少少了解了一點兒KMP,至少已經聽聞KMP匹配很快。本文不做嚴格的證明,只是幫助你理解KMP,以免像我一樣,學了之後,不久就又忘了。
KMP爲什麼比較快?像這樣:
當比較到e和d的時候,沒有必要從i=1和j=0開始,直接變成這種情況:
因爲之前的比較已經知道e前的ab和d前的ab一樣,而第二串(也就是模式串T,第一串稱爲目標串S)的開始也是ab,所以c之前的字符和e之前的字符一樣。所以j可以直接從d跳回到c,拿e和c比較,顯然也是不相同的的,之後的過程和這個相同。
可以看到i從來都沒有後退過(至於爲什麼i可以不回到1進行匹配,請參見算法書的相關章節),所以查找的時間就是目標串S的長度n。在查找之前需要預處理模式串T,時間是模式串的長度m(後面會說到),所以模式匹配的時間複雜度是O(n+m)。而以前的複雜度是O(n*m),所以快了不少。
這個算法需要一個額外的數據,就是next[m]數組,是通過分析模式串T得到的匹配不成功時的跳轉信息,next[j]表示下標爲j的字符與目標串不匹配時,需要跳到下標next[j]處。next[0]=-1。
匹配
通過上面的分析,可以得到如下匹配代碼:
int find(char *s1,char *s2,int len1,int len2)
{
int i,j;
i=j=0;
while(i<len1&&j<len2)
{
if(j==-1 || s1[i]==s2[j])
{
++i;
++j;
}else
{
j=next[j];
}
}
if(j==len2)
{
return i-len2;
}else
{
return -1;
}
}
s1是目標串,s2是模式串,len1是s1的長度,len2是s2的長度。
當s1[i]==s2[j]的時候,++i;++j很好理解。
但是當j==-1的時候怎麼回事兒呢?-1是通過j=next[j]得到的,即s1[i]與s2[0]不相等的時候,j=next[0];(next[0]=-1上面有說到)這樣j就變成-1了。就是說s1[i]一直匹配不成功,連s2[0]都沒有匹配成功。所以就不再拿s1[i]匹配了,++i表示從下一個開始匹配。++j,剛好j==0。所以j==-1的時候,也需要++i;++j。
這就是在s1中尋找s2的過程。簡單吧。
預處理
下面來說說預處理s2的過程,也就是求next數組。
回顧最前面的查找過程的講解
之所以可以從d跳回到c,是因爲他們之前都有ab,所以next[5]=2。那個求next數組的過程,就是在當前字符之前找一個串是模式串的前綴。在模式串中匹配它的前綴,這本身就是模式匹配,所以求next數組的代碼和前面的匹配代碼基本上一樣。(這也就可以理解,預處理的複雜度是O(m) )
void getnext(char *s)
{
int i=0,j=-1;
next[0]=-1;
while(s[i])
{
if(j==-1 || s[i]==s[j])
{
++i;
++j;
next[i]=j;
}else
{
j=next[j];
}
}
}
當存在一個以s[i]結尾的串和串s[0]…s[j]相同的時候(也就是代碼中的s[i]==s[j],s[i-1]==s[j-1]等判斷在之前的循環中已判斷過),那麼,就可以從j+1跳回i+1,所以有++i; ++j;next[i]=j;至於爲什麼j==-1也可以,參照之前對j==-1的分析。
現在再說說,爲什麼j從-1開始。記得在匹配的代碼中,i和j都是從0開始的。在開始的時候,i=0,想要找一個以s[i=0]結尾的串和模式串的前綴相同,這是不存在的,只有一個s[0],找不到兩個串(注意,這兩個串不能從相同的地方開始),-1就表示匹配失敗。i=0的時候,匹配一定是失敗的,所以j一定是-1。
現在,基礎的KMP已經講完了。來個例子:
當第二個b匹配失敗的時候,因爲他之前的a和前綴a一樣,所以可以跳回到第一個b,下標爲1。當d匹配失敗的時候,他之前的ab和前綴ab一樣,所以可以跳回到c,下標爲2。
next數組的改進
繼續分析上面的例子。
當第二個b匹配失敗的時候,因爲他之前的a和前綴a一樣,所以可以跳回到第一個b,下標爲1。此時,還是字符b,顯然,還是匹配失敗,然後再跳到下標爲0的位置。可以看出來,當跳轉到的字符和當前字符一樣的時候,需要繼續跳轉。改進就是讓跳轉一步到位。代碼如下:
void getnext2(char *s)
{
int i=0,j=-1;
next[0]=-1;
while(s[i])
{
if(j==-1 || s[i]==s[j])
{
++i;
++j;
if(s[i]!=s[j])
{
next[i]=j;
}else
{
next[i]=next[j];
}
}else
{
j=next[j];
}
}
}
看,當s[i]==s[j]的時候,next[i]=next[j];i不是跳轉到j,而是跳轉到j應該跳轉到的地方。
新的next數組是:
至於爲什麼之前的next數組只有一個-1,而新的next數組有兩個,自己思考下吧。還有,之前求next數組的方法,對任意字符串來說是否只有一個-1,爲什麼?
完整代碼:
#include<stdio.h>
#include<string.h>
int next[100];
void getnext(char *s)
{
int i=0,j=-1;
next[0]=-1;
while(s[i])
{
if(j==-1 || s[i]==s[j])
{
++i;
++j;
next[i]=j;
}else
{
j=next[j];
}
}
}
void getnext2(char *s)
{
int i=0,j=-1;
next[0]=-1;
while(s[i])
{
if(j==-1 || s[i]==s[j])
{
++i;
++j;
if(s[i]!=s[j])
{
next[i]=j;
}else
{
next[i]=next[j];
}
}else
{
j=next[j];
}
}
}
int find(char *s1,char *s2,int len1,int len2)
{
int i,j;
i=j=0;
while(i<len1&&j<len2)
{
if(j==-1 || s1[i]==s2[j])
{
++i;
++j;
}else
{
j=next[j];
}
}
if(j==len2)
{
return i-len2;
}else
{
return -1;
}
}
int main()
{
int i,j,len1,len2;
char s1[100],s2[100];
while(gets(s1))
{
gets(s2);
len1=strlen(s1);
len2=strlen(s2);
getnext2(s2);
printf("\n模式串:");puts(s2);
printf("next[]:");
for(i=0;i<len2;++i)
{
if(next[i]==-1)
{
printf("&");
continue;
}
printf("%d",next[i]);
}
printf("\n&表示-1\n\n");
int from=find(s1,s2,len1,len2);
if(from!=-1)
{
printf("Yes. From %d to %d\n",from,from+len2-1);
}else
{
printf("No\n");
}
}
return 0;
}