kmp算法详解

最近数据结构课上讲到了kmp,我就来复习一下,毕竟暑假的时候搞了一整天,太折磨了,所以还是有一些心得的。

首先,我默认大家都会暴力的做法了啊,我就直接进入正题。

所谓kmp,其实没有那么复杂,目的是在a串中找到b串第一次出现的位置,方法总结下来就一句话:充分利用模式串(目标串)的特性,减少匹配次数。

那什么叫做模式串的特性呢,咱们先看第一个例子:

待匹配串(A):abcabcde

模式串(B):abcde

首先我们还是和暴力的思路一样从第一位(A[0],B[0])开始匹配,直到下面这个情况:

这时我们发现A[i]与B[j]不相等,如果按照暴力的做法我们应该让i回到A[1],j回到B[0],但是,我们仔细观察这个目标串,发现它的已经做过匹配的子串(B[0]-B[j-1],就是abc,注意,不是abcd,而是abc)有一个特性:没有重复的字母!但这又能说明什么呢?别急,我们模拟一下暴力的做法:

按照暴力的做法我们接下来会执行如下操作:

这时我们会发现,我标记出来的那一位是无论如何也不可能匹配正确的,为什么呢?相信你们都想到了,对,就是我们刚才说的,abc这个字串的特性:没有一个和c相等,那么我们就可以省去上面的几步,直接开始下面这次匹配:

如果你还不清楚为什么可以省去上面几步的话,请你伸出自己可爱的小手,动笔模拟一下上面两个字符串暴力匹配的过程,并把目光着重放在第三个位置上。

如果已经清楚了,那我们就开始下一个例子:

待匹配串(A):abaabaabeca

模式串(B):abaabe

同样我们先从第一个开始匹配,直到下面这个情况:

这时我们也像第一个例子那样思考一下子串abaab(不是abaabe)的特征,我们发现,它开头有一个ab(前缀),末尾也有一个ab(后缀),并且没有比ab更长的相同的前后缀了。(前缀--第一个到最后一个之前的任意一个,后缀--第一个之后的任意一个到最后一个)。接着我们再去模拟一下暴力的过程,我们发现,从上图到下图中间的所有的过程都可以省略,因为不论如何,不可能有别的子串再与ab相匹配。

看到这里,相信聪明的你们已经悟了,没悟道也没关系,还是像上面一样,动手模拟一下暴力的过程,目光聚焦在第三第四个位置。

好!如果上面两个例子你看明白了,那kmp算法你就已经懂了。

首先我们需要一个数组(一般都命名为next,但是next是c++的一个库函数,所以我一般写_next或者Next)Next[n],n是模式串的长度,其中Next[i]代表模式串的子串A[0]~A[i]的特征:相同的前缀和后缀的最大长度。比如aba的最长相同前后缀就是a,长度为1(注意后缀的定义,ab不是后缀,ba才是,后缀也是从前往后看,只是终点在末尾)。我们先不去像这个数组怎么用程序求出来,我们就假设我们已经有这个Next数组了,然后我们回到上面的例二,你可以试着将例二的模式串的Next数组写出来。

也许你自己写了,也许没有,但是你一定都有一个疑问:为什么最后一个我没有写。我们回看一下前两个例子你就会发现,我一直在强调我们要找的特征串是当前匹配不上的那个位置的前面的子串,因此最后一个的Next的值永远都不会被用到,所以不用赋值,当然你写了也没有关系。

有了Next数组,我们再去做一下例二,还是先暴力匹配到这里:

好,按照我们上次做的,我们这时候应该去找子串abaab的特征,而这个特征在这里已经量化成了Next数组,Next[j-1]=2,代表B[0]~B[1]==B[j-3]~B[j-2] (并且也只有B[0]~B[1]才等于B[j-3]~B[j-2])所以我们直接让j=Next[j-1],便跳过了无需比较的几个情况,直接到了下图:

这就是kmp的全过程,将目标串的每个子串的特征量化到特征数组Next中,比较过程中,i无需后退,j根据Next数组省略掉尽可能多的比较步骤。

听到这里你是不是觉得,就这?爷会了。不不不,kmp真正的难点还在下面,也就是我们刚才遗留的Next数组的求法,或许你用眼睛看会觉得很简单,但是想要用代码实现出来还是不太容易的。这里需要用到一点点动态规划的思想,何为动态规划,说得直白一点就是递推,我知道了子问题的解,怎么样求得整个问题的解。我们一点一点来:

首先明确我们的目标:提取目标串的特征并存到Next数组中(最长相等前后缀的长度)。

我们假设现在已经算到了第j步(这样假设的原因在于我们把过程一般化,更好得到普遍结论,听不懂也没关系,不影响你理解kmp,后面学到动态规划的时候就会明白了),换句话说,我已经知道了(0~j-1)的所有Next的值,那我们设Next[j-1]=k.(k是一个已知的量),即下图所示:

然后我们要根据Next[j-1]的值去计算Next[j],我们想想啊,Next[j]这个状态无非就比Next[j-1]多了一个B[j],那我们就重点去看这个B[j],无非就下图两种情况:

可能你还没有看出来,没关系我来给你稍微解释一下相信你就明白了,来,上图:

我们第一次做的时候分析过1和2是子串abaab的最长相同前后缀,但是注意一下3这个子串,它是已经经过匹配,确定了和2相等的,那它是不是可以看作子串abaab(注意,这个子串指的是A的子串)的后缀,那我们这个特征是不是可以转化成A的子串(abaab)和B的子串(abaab)的最长相等前后缀。

再看我们刚才第二种情况转化成的问题,是不是一模一样,而我们在例二中怎么解决这个问题的?是不是让j回退到Next[j-1]的位置,然后继续匹配?这里也是一样,只不过这里的k才是j的含义,我们让k回退到Next[k-1]的位置继续完成匹配即可,那Next[k-1]的值怎么求呢?看看我们的假设,由于k一定小于j(k是前缀嘛,肯定到不了最后一个),所以我们已经知道了Next[k-1]的值,直接跳转过去就可以了。我们假设Next[k-1]的值是m这时问题又转化了:

是不是又很眼熟,这不跟第一次一样也是两种情况吗?无非(b(m)=b(j)或者b(m)!=b(j)),相信聪明的你一定已经知道这是个什么样的程序结构了,我猜你会说,递归!确实可以,但没必要,我们用一个while循环就可以解决这个问题了。

终于到了上代码的时候了,相信一路看下来的你已经对kmp有了一定的了解了,代码实现还不是有手就行?

但是这里还有一个小细节要说明一下,我们之前一直将Next数组定义为下面这样,这种做法便于理解但是代码实现的时候会造成麻烦,想知道会造成什么麻烦的小伙伴自己实现一下就知道了,嘻嘻。

所以我们采用下面这种标记法,将Next数组后移一位,第一位赋值为-1,至于为什么赋值为-1,看代码就懂了。

//csdn-wuhudaduizhang

#include <bits/stdc++.h>
using namespace std;

void Getnext(string t,int Next[])
{
   int j=0,k=-1;
   Next[0]=-1;
   while(j<(int)t.size()-1)
   {
       //嘻嘻,看这里如果我们还按照原来的方法写0的话就会造成死循环,但也不是不能处理,只是代码会有些丑陋
      if(k == -1 || t[j] == t[k])
      {
         j++;k++;
         if(t[j]==t[k]){
			Next[j]=Next[k];
         }else{
            Next[j] = k;
		}
      }
      else k = Next[k];//最后一位匹配不上就不断回退
   }
}

int KMP(string a,string b)
{
   int Next[a.size()],i=0,j=0;
   Getnext(b,Next);
   while(i<(int)a.size()&&j<(int)b.size())
   {
	    if(j==-1 || a[i]==b[j])
	    {
	        i++;
	        j++;
	    }else{
	    	j=Next[j];  //j回退
		}          
   }
   if(j>=(int)b.size())
       return (i-(int)b.size());         //匹配成功,返回子串的位置,注意下标从0开始
   else
      return (-1);                  //a搜到头了b都没搜到头,就说明没找到,返回-1
}

int main(){
	string a,b;
	a="abaabaabeca";
	b="abaabe";
	cout<<KMP(a,b)<<endl;
	return 0;
}

同学给我检查出了一个很离谱的bug,size()函数返回的是unsigned int 如果直接和-1比较的话会出错,所以加上一个强制类型转换。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wuhudaduizhang

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

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

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

打赏作者

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

抵扣说明:

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

余额充值