KMP的那点事(庖丁解牛) 一篇文章 彻底看懂KMP算法

背景介绍:

上述题目就是经典的KMP算法的模版题

首先我们介绍传统的暴力做法,不过显示是ac不了这道题的 

暴力做法:  两个串依次比较

 该算法的时间复杂度对于本题而言 就是O(mn

如何优化呢?

对于下面这种情况而言:

 当浅蓝色框内的部分匹配,A[5]!=B[4]时:

暴力做法 就是  A串 i=2 这个元素  与   B串首字母  重新进行匹配

显然 这个过程每次遇到不匹配的 都是A串向前一位  B串从头开始比较  非常繁琐 

故这里 就有大佬创造了   KMP算法!!!!!!!!!!

其核心就是优化上述 过程

先简单解释一下 KMP算法 主要做了什么工作哈

a b串 黑色线之间部分已经匹配,后面一个元素(绿色圈里面的)不匹配

此时暴力做法就是: 

 a串向后移动一个位置,与b串从头开始比较,及也就是从紫线部分从头开始比较

KMP的思想就是,a串向后最多能移动几个位置,而保证不出错。

这里引入   最长 前缀  后缀  相等   的概念  (这里就不详细介绍了  帖子很多 也很简单  以下用数组ne[]表示)

首先证明  x的 存在且必要

(这部分绿线长度为非负  >=0)即就是 不存在匹配的 前后缀的串

又因为黑色线之间的部分  a b串本身就是匹配的  故可以得到:

ZW这两部分 是匹配的 

因为x是非负的  故一定存在

此时直接比较红色框内的这部分即可  因为前面已经是配的了    

一次KMP在字符串匹配上面的效率很快  时间复杂度  O(n)!!!!!

以下这部分 重中之重!!!!!!!-

理解了 KMP算法的思想    下面主要就是求 ne数组     存储前后串匹配位置的数组即可

用到的方法就是 递推的方法  

首先解释一下 ne数组  里面的数字表示啥:  假设ne[ j ]=k;

意思是: 数组下标  从  0 1  2 一直到 j的这部分  其  前缀 后缀 的长度为 k+1

 有了这个概念    我们定义不匹配为 -1  表达式ne【j】=k;    且前缀后缀长度均小于其自身 即 k<j 

所以已知 ne【0】= -1;

假设j=n时已知,ne【n】=x

红的括号内的两部分 匹配       即为前缀 后缀 字符串   (且为最长哈  定义保证了)

注释:其中 n x 为数组下标    且数组下标 从0开始

+

来计算j=n+1时                         有点数学归纳法的意思了哈   这也是 为啥代码体现为 递推 

这里就需要分情况讨论了:

第一种情况:arr【n+1】==arr【x+1】

   此时ne【n+1】=x+1  结束

第二种情况:arr【n+1】!=arr【x+1】

此时就要自己去寻找 0~ n+1 下标是的  最长相等前后缀的位置了 

暴力做法(为什么每次都要提到暴力做法,因为好的做法 都是从暴力做法去优化出来的!!!!

首先看 挪一步  是否匹配:

挪动两步:

………………

依次向下总到找到  ne【n+1】=y; y=-1,0,1,2……x  最大取到x嘛   这里范围也不重要 反正能找到就行了。

观察如下关系:

A点坐标n    c点坐标为x      B点坐标为y-1   

绿线与arr的三个焦点分别为1 2 3  注意 不是坐标哈

已知的ne【n】=x (我们的假设 别忘了) 

所以:1A段与2C段是匹配的

又因为   ne【n+1】=y

所以:1A段与3B段是匹配的

所以2C段与3B段也是匹配的  所以这个东西出来  你们有什么感悟嘛   !!

是不是关系就得出来了:

即 B点是A点  不断ne【】  取出来的结果    结束条件就是arr【n+1】==arr【y】 当然了 y=-1的话 就是不存在这个点  (整个移到头就行)

当然了 也有找不到的情况:

就是到这  应该理解ne数组怎么求了 

核心思想就是 递推 直到ne【0】 ne【1】  可以推出ne【2】……一直递推

 

    //求Next数组 表示最长 相等前缀后缀字符串
    for(int i=1,j=0;i<=n-1;++i)
    {
      while(j>0 && arr[i]!=arr[j] )
           j=Next[j-1]+1;
      if( arr[i]==arr[j])
          j++;
      Next[i]=j-1;
    }

解释一下:   因为ne【0】=-1  这是已知的 所以 i就从1 开始     也可以理解为上下要错开一个 

 

代码不要从头开始 分析   只要记得  我们求 i这个位置时(求ne【i】)   前面 0~i-1 是已知的就行

这样方便初学者理解

所以这一步  就是我们上面提到的

结束条件有两个  找到了这两个点相等 或者就是没找到   因为没找到  ne【】=-1   所以j=0;

这个意思就是:  经过上面我找到了  他两匹配   是不是就要看下一位是不是匹配 所以j++了    因为i++  会在循环条件里面判断 所以这里就不用多判断了

这里最后一步就是    因为上面我们找到了 ne【i】的值 所以来给他赋值

为什么减一呢   因为上面我们有个if 使得j++了  

如果j=0 就是没找到 匹配的        所以赋值为-1;

看到这里ne数组就取出来了

最后就是  字符串的匹配过程: 

#include<iostream>
using namespace std;

const int M=1000010;
const int N=100010;
char arr[N],brr[M];
int Next[N];

int main()
{
    Next[0]=-1;
    int n,m;
    cin>>n;
    scanf("%s",arr);
    cin>>m;
    scanf("%s",brr);
    //求Next数组 表示最长 相等前缀后缀字符串
    for(int i=1,j=0;i<=n-1;++i)
    {
      while(j>0 && arr[i]!=arr[j] )
           j=Next[j-1]+1;
      if( arr[i]==arr[j])
          j++;
      Next[i]=j-1;
    }
    
    


    //匹配过程
    for(int i=0,j=0;i<=m-1;++i)
    {
    
      while(j>0 && brr[i]!=arr[j])
          j=Next[j-1]+1;
    
      if( brr[i]==arr[j])
          j++;
    
        if(j==n)
      {
      printf("%d ",i-n+1);  
         j=Next[j-1]+1;
      }
  
    }
    
    
    
    
    
    return 0;
}

brr为 我们被搜索的 数组                   arr为我们搜索的数组

匹配过程和 求ne完全类似           因为我们求ne其实就是 两个数组来做的  这里只不过就是两个数组不一样了

这里还是别从头分析  只要写出递推关系就行了 

假设i-1     j-1 前面部分匹配   来看i  j 两个位置      如果不相等 就一直取arr的  ne数组就行 直到这两个位置匹配 或者 没找到匹配的

如果匹配了   就直接就看下一个位置就行

这里的判断条件 就与上面求ne数组的地方不太一样

主要就是看我们arr的数组是不是匹配完了

下标都是从零开始   到n-1   这里为什么判断n呢

因为 上面的if条件  如果arr【n-1】匹配了     那就会使用j++    所以会多一个   

然后就是打印 brr匹配的起始下标

这里的下一步就是 接着搜索

首先来看一下暴力的话 这一步怎么写

for(int i=0,j=0;i<=m-1;++i)
    {
    
      while(j>0 && brr[i]!=arr[j])
          j=Next[j-1]+1;
    
      if( brr[i]==arr[j])
          j++;
    
        if(j==n)
      {
      printf("%d ",i-n+1);  
       i=i-n+1;
      j=0;
      }
  
    }

就是 brr数组 从匹配位置的下一个点   和   arr从头开始  我们最开始的那个过程 

怎么优化呢   画个图就明白了

这两部分已经匹配完成

下一部分匹配 无非就是这两种情况:

观察左边那一个  不就是 前后缀匹配 ne嘛 

右边那一个不就是  ne到-1了么

所以这里就直接优化

 就行 

最后放一下  题目ac的截图吧

KMP算法 就我自己第一遍学习  编写了此博客   欢迎各位大佬批评指正  

我是这么理解 KMP算法的 以及编写代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值