KMP详解

KMP详解

        既然你已经找到这儿了,说明你已经多多少少了解了一点儿KMP,至少已经听闻KMP匹配很快。本文不做严格的证明,只是帮助你理解KMP,以免像我一样,学了之后,不久就又忘了。

KMP为什么比较快?像这样:

KMP详解

 

当比较到ed的时候,没有必要从i=1和j=0开始,直接变成这种情况:

KMP详解

 

因为之前的比较已经知道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数组。

回顾最前面的查找过程的讲解

KMP详解

 

之所以可以从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已经讲完了。来个例子:

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数组是:

KMP详解

 

至于为什么之前的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;

}



你可能感兴趣的:(KMP)