KMP詳解

KMP詳解

        既然你已經找到這兒了,說明你已經多多少少了解了一點兒KMP,至少已經聽聞KMP匹配很快。本文不做嚴格的證明,只是幫助你理解KMP,以免像我一樣,學了之後,不久就又忘了。

KMP爲什麼比較快?像這樣:


當比較到ed的時候,沒有必要從i=1和j=0開始,直接變成這種情況:

因爲之前的比較已經知道e前的abd前的ab一樣,而第二串(也就是模式串T,第一串稱爲目標串S)的開始也是ab,所以c之前的字符和e之前的字符一樣。所以j可以直接從d跳回到c,拿ec比較,顯然也是不相同的的,之後的過程和這個相同。

可以看到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;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章