现在的位置: 首页 > 综合 > 正文

KMP详解

2013年10月11日 ⁄ 综合 ⁄ 共 2962字 ⁄ 字号 评论关闭

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;
}

抱歉!评论已关闭.