KMP算法终结篇(一)

KMP算法简介
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。

下面开始分步讲解一下KMP算法的模式

	从模式串t中提取加速匹配的信息

在KMP算法中,通过分析模式串t,从中提取出加速匹配的有用信息。这种信息是对于每一个t的每个字符t[j](0<=j<=m-1)存在一个整数k(k<j),使得模式串t中开头的k个字符依次与t[j]的前面k个字符相同,如果k有多个值,k取其中最大的一个值。模式串t中的每一个位置j的字符都有这样的信息,采用next数组表示,即是next[j]=Max(k);

下面来举一个例子
模式串t=“aaab" ,对于j=3,就是模式串中下标为3的字符t[3]=”b“,观察前面的字符串,可以得到t[2]=t[0]=“a”。

  1. a aab 加了删除线的为t[0]
  2. aaa b 加了删除线的为t[2]

· 可得 k=1;

  1. aa ab 加了删除线的为t[0]t[1]
  2. aaa b 加了删除线的为t[1]t[2]

可得k=2;

所以next[3]=Max{1.2}=2;

归纳next数组取值的规律
在这里插入图片描述

next的求解过程如下(图文并茂)

  1. next[0]=-1,next[1]=0,
  2. 如果next[j]=k,表示有”t[0]t[1]…t[k-1]“=“t[j-k]t[j-k+1]…t[j-1]”
    1.如果t[k]=t[j], 即是”t[0]t[1]…t[k-1]t[k]“=“t[j-k]t[j-k+1]…t[j-1]t[j]”,显然有next[j+1]=k+1.
    2.如果t[k]!=t[j],说明t[j]之前不存在长度为next[j]+1的字串和开头字符起的子串相同,那么是否存在一个 长度较短的子串和开头字符起的子串相同呢?设k’=next[k],则下一步是应该将t[j]与t[k’]比较:若两者相同,则说明tj之前存在长度为next[k’]+1的子串和开头字符起的子串相同;否则依次类推寻找更短的子串,直到不存在可匹配的子串,置next[j+1]=0。所以当t[k]!=t[j],置k=next[k]。

对应的代码如下:

void getnext(sqstring t,int next[])
{
 int j,k;
 j=0;k=-1;next[0]=-1;
 while(j<t.length-1)
 {
  if(k==-1||t.data[j]==t.data[k])
  {
   j++;k++;
   next[j]=k;
  }
  else
  k=next[k];
 }
}
	KMP算法的模式匹配过程

求出模式串t的next数组后,你也许还不明白next数组的真正作用是什么。但是下面我会一一讲解,先总结一下,它是用来消除主串指针的回溯。下面我来举一个例子。
以目标串s=“aaaaab” 模式串t=“aaab”
第一趟匹配:从i=0,j=0,开始,不匹配处是i=3,j=3,虽然这次匹配失败了,可以得到部分匹配信息:s1s2与t1t2相同,如图所示
在这里插入图片描述

而模式串t中有next[3]=2,表明t1t2=t0t1,所以有s1s2=t0t1;
在这里插入图片描述

原来第二趟匹配是需要从i=1,j=0开始的,即需要回溯,现在既然有s1s2=t0t1,第二趟匹配可以从i=3,j=2开始,即保持主串指针i不变,模式t右滑动1(=j-next[3])个位置,让si和tnext[j]对齐进行比较。
在这里插入图片描述

下面来讨论一般情况。设置目标串s=“s0s1…sn-1”,模式串t=“t0t1…tm-1”,在进行第i-j+1趟时出现的不匹配情况

  目标串s:  s0  s1....si-j  si-j+1 .. si-1    **si**     si+1.....sn-1
  							
  模式串t:             t0     t1.....tj-1     **tj**     tj+1......tm-1

发生不匹配的为si!=tj,这时候发生部分匹配是“t0t1…tj-1”=“si-jsi-j+1…si-1”,

显然在k<j时有:tj-ktj-k+1…tj-1=si-ksi-k+1…si-1
在这里插入图片描述

因为next[j]=k,即:

t0t1…tk-1=tj-k tj-k+1…tj-1

由上面两个公式可得“t0t1…tk-1”=“si-ksi-k+1…si-1”,
在这里插入图片描述

下一趟就不在从si-j+1开始匹配,而是从si-k开始匹配,并且直接将si与tk进行比较,这样就可以把i-j+1趟比较失败时的模式串t从当前位置直接右滑动j-k个字符,如下图所示。
在这里插入图片描述

上述过程中,从第i-j+1趟直接转到第i-k+1趟匹配,中间可能遗漏一些匹配趟数,那么KMP算法是否对呢?实际上,因为next数组next[j]=k,容易证明中间的匹配趟数是没有必要的。

下面我们通过一个实例验证,设目标串s=“s0 s1 s2 s3 s4 s5 s6”,模式串t=“t0 t1 t2 t3 t4 t5”,next[5]=2,从s1开始匹配,不匹配处为s6!=t5,这里 i=6,j=5,k=2,下面说明第i-j+2(=3)到第i-k(=4)趟是不必要的。

如下图所示,部分匹配信息有“t1t2t3t4”=“s2s3s4s5“,因为next[5]=2,有”t0t1“=“t3t4”,同时有”t1t2t3t4“!=“t1t2t3t4”(若不相同,则next[5]=4,而不是2),从而推出”s2s3s4s5“!=“t0t1t2t3”。所以从s2开始匹配是不必要的。
在这里插入图片描述

同样的道理,因为next[5]=2,有“t0t1t2”!=“t2t3t4”,(若相同则next[5]=3),推出从s3开始匹配是不必要的,下一趟应该从s4开始匹配,而且是直接将s6与t2进行比较。
所以,当模式串t中t0与目标串s中某个字符si不匹配时,用next[0]=-1,表示t中已经没有字符与当前字符si进行比较了。i应该移动到目标串s的下一字符,再和模式串t中的第一个字符进行比较。

KMP算法的过程如下:

//sqstring在这里我设置的是一个结构体
int KMPIndex(sqstring s,sqstring t)
{
 int i=0,j=0;
 int next[MaxSize];
 getnext(t,next);
 while(i<s.length&&j<t.length)
 {
  if(j==-1||s.data[i]==t.data[j])
  {
   i++;
   j++;
  }
  else
  j=next[j];
 }
 if(j>=t.length)
 return i-t.length;
 else
 return -1;
}

设主串s的长度为n,子串t的长度为m,KMP算法的平均时间复杂度为O(n+m)。

		改进后的KMP算法

首先我们来看一下上面算法的缺陷
在这里插入图片描述

上述的模式串的next[4]={-1,0,0,1}; 当比较到i,j时发生不匹配,此时next[j]=1,所以应该将i保持不变,j移动到1的位置。
在这里插入图片描述

可以发现这里明显还是不匹配,完全没有意义,因为前一个步骤中后面的B已经不匹配了,前面一个B也是不可能匹配的,同样的情况其实还发生在第2个元素A上。

这里我们可以观察到问题的原因:t[j]==t[next[j]]
这也就是说,按照我们前面的原因得到next[j]=k,在模式串中有t[j]=t[k],当目标串中的字符s[i]与模式串中的t[j],比较不同时,s[i]!=t[k],所以没有必要再将s[i]和t[k]进行比较,而是直接将s[i]与t[next[k]]进行比较,为此这里我们把next[j]修改为nextval[j].
nextval数组的定义是nextval[0]=-1,当t[j]=t[next[j]],nextval[j]=nextval[next[j]],否则nextval[j]=next[j];

用nextval取代next,得到改进的KMP算法如下:


void Getnextval(int nextval[],String t)
{
   int j=0,k=-1;
   nextval[0]=-1;
   while(j<t.length)
   {
      if(k == -1 || t[j] == t[k])
      {
         j++;k++;
         if(t[j]!=t[k])//当两个字符相同时,就跳过
            nextval[j] = k;
         else
            nextval[j] = nextval[k];
      }
      else k = nextval[k];
   }
}
public int KMPIndex(String s, String t) {
    int nextval[Maxsize];
    int i=0,j=0;
    Getnextval(t,nextval);
    while(i<s.length&&j<t.length)
    {
     if(j==-1||s.data[i]==t.data[j])
     {
      i++;
       j++;
      }
  else
  j=nextval[j];
 }
 if(j>=t.length)
 return i-t.length;
 else
 return -1;
    }

完整c语言实现 未改进优化的

#include<stdio.h>
#include<string.h> 
struct sqstring
{
 char data[100];
 int length;
};
int next[100];
sqstring s;
sqstring t;
void getnext(sqstring t,int next[])
{
 int j,k;
 j=0;k=-1;next[0]=-1;
 while(j<t.length-1)
 {
  if(k==-1||t.data[j]==t.data[k])
  {
   j++;k++;
   next[j]=k;
  }
  else
  k=next[k];
 } 
}
int KMPIndex(sqstring s,sqstring t)
{
 int i=0,j=0;
 getnext(t,next);
 while(i<s.length&&j<t.length)
 {
  if(j==-1||s.data[i]==t.data[j])
  {
   i++;
   j++;
  }
  else
  j=next[j];
 }
 if(j>=t.length)
 return i-t.length;
}
int main()
{
 int location;
 scanf("%s",t.data);
 t.length=strlen(t.data);
 scanf("%s",s.data);
 s.length=strlen(s.data);
 location=KMPIndex(s,t);
 printf("匹配字符串的首地址是:%d",location);
 return 0;
 } 

运行截图

在这里插入图片描述
算法复杂度与前者一样都为O(n+m)

java实现参考KMP算法图解

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Devin Dever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值