zoukankan      html  css  js  c++  java
  • 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("
    模式串:");puts(s2);
    		printf("next[]:");
    		for(i=0;i<len2;++i)
    		{
    			if(next[i]==-1)
    			{
    				printf("&");
    				continue;
    			}
    			printf("%d",next[i]);
    		}
    		printf("
    &表示-1
    
    ");
    		int from=find(s1,s2,len1,len2);
    		if(from!=-1)
    		{
    			printf("Yes. From %d to %d
    ",from,from+len2-1);
    		}else
    		{
    			printf("No
    ");
    		}
    	}
    	return 0;
    }



  • 相关阅读:
    vue-router 路由拦截 beforeEach 添加静态路由 与 动态添加路由
    elementUI el-upload 根据上传的图片高度,进行自适应宽度
    vue 中 字符串分两行显示
    MySQL中的<=>
    Spring mvc再启动时候会打印项里面的所有路径
    一次解决前后台交互问题
    数据库表分区,分表
    支付宝接口
    打印js中一个对象的所有属性的值
    var
  • 原文地址:https://www.cnblogs.com/riskyer/p/3318150.html
Copyright © 2011-2022 走看看