KMP算法详解,再看不懂打死我算了

用于字符串匹配的KMP算法,算是我见过最难理解的算法之一。为了看懂这个算法,花了不少时间,查了不少资料。本文的重点在于对算法中难理解的的地方做了一些说明,而对算法本身是如何实现的不作过多的说明。现在整理如下,希望对大家理解这个算法有帮助。

预备知识
1、最长公共前后缀
字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。例如对于字符串 abacaba,其前缀有 a, ab, aba, abac, abacab,后缀有bacaba, acaba, caba, aba, ba, a。最长公共前后缀就是 aba。
————————————————

2、最长公共前后缀长度
最长公共前后缀长度(后面为了方便,简称最大长度),是KMP算法的一个关键的概念,而求解最大长度也是KMP算法中最关键、最难理解的一步。
对于字符串中的任意一个字符j,,如果如果前缀 [0, k-1] 与后缀 [j-k, j-1]完全相同,那么公共子串长度就是k。如果存在多个这样的k,那么最大的就是最大子串长度。并且用next[j] 表示字符j对应最大子串长度。
在这里插入图片描述
对于字符str[6],由于前缀[0,2]与后缀[3,5]相等,因此next[6] = 3,同理:
对于str[5]来说,由于前缀[0,1]与后缀[3,4]相等,因此next[4] = 2
对于str[4]来说,由于前缀[0,0]与后缀[3,3]相等,因此next[4] = 1
对于str[3]来说,不存在公共前缀,因此,next[3] = 0

需要说明的是,后缀的起始位置最小为1,因此索引位置为0和1的位置不存在公共字符串,next[1] = 0 , next[0] = 0。但是为了后续计算方便,设next[0] = -1。
2、最大子串长度的用途
字符串暴力匹配算法中,一旦某个字符不匹配,目标串和模式串指针都会回退。
如下图所示,如果匹配到模式串j时,发现 [i] != [j], 那么目标串就会移动到原来起始位置的下一个位置,即i - j + 1,而模式串又会从头开始匹配。
在这里插入图片描述
目标串起始位置向下移动一个位置,模式串又从头开始:
在这里插入图片描述
由于目标串指针和模式串指针都向前进行了回溯,显示效率不高。直觉告诉我们,肯定不用从头再来。比如,按照下面这种方式移动:
让模式串j的最长公共前缀与目标串的后缀重合。
(注意:由于目标串 [i-j,i-1] 与 模式串 [0,j-1] 是完全匹配的,因此模式串j的最长公共子串与目标串 [i-j, i] 的最长公共子串是一样的)
在这里插入图片描述

按照上面这样移动之后,目标串的指针i就不用回溯了,而模式串的指针回溯到位置k就可以了。k到底是多少?
稍微想一下就会知道,k = next[j] !
这就是为什么要求解模式串的每一个字符对应的最大子串长度next[j],简单来说,如果模式串位置j的最大子串长度为next[j] = k,如果匹配到位置j时发生不匹配,那么模式串只用回溯到位置k就好,而目标串的位置不用回溯。显然这么多提高了匹配效率

很多人可能会疑惑,按照上面这种方法,虽然提高了匹配效率,会不会遗漏?
在暴力解法中,如果目标串中起始位置为i-j的子串与模式串不匹配后,会对下一个位置i-j+1的子串进行匹配。而KMP算法,直接将下一个位置移动到了i-k。那么问题来了?
在目标串中起始位置介于[i-j+1,i-k)的子串有没有可能与模式串匹配呢
绝无可能!证明如下:
如下图所示,如果以目标串[i-j+1,i-k)中某个位置p为起始位置的子串能够与模式串匹配,那么两条红色虚线之间的部分肯定也会匹配,说明模式串的j的最大公共子串长度大于k,与j的最大公共子串长度为k矛盾
模式串j原来的最大的子串为[0,k),对应目标串为[i-k,i)
按照现在这种假设,j在目标串对应的最大子串为[p,i) (i-j+1 <= p < i-k)
在这里插入图片描述

3、最大子串长度的求解
参考 文末参考文章【1】,里面已经说得很清楚,不再赘述。
直接附上求解next的算法:

void Getnext(int next[],String t)
{
   int j=0,k=-1;
   next[0]=-1;
   while(j<t.length-1)
   {
      if(k == -1 || t[j] == t[k])
      {
         j++;k++;
         next[j] = k;
      }
      else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!
   }
}

4、优化后的KMP算法
考虑下面一种情况,模式串 t[j + 1] = t[k],在 j+1时不匹配。
如下图所示,如果模式串匹配到 j+1 时,发现模式串与目标串不匹配:
在这里插入图片描述

根据改进的算法,会移动模式串,使模式串的位置 k 与目标串的 i 对齐:
在这里插入图片描述
显然这一步是多余的,因为移动之前,由于模式串 t[j+1] != s[i],需要移动模式串,使位置 k 与目标串 i 对齐。由于 t[j+1] = t[k],因此 如果 t[j+1] != s[i],那么必然有t[k] != s[i]。也就是说,移动后模式串与目标串仍然不会匹配,那么这一步的移动就是多余的。

那么问题出在那呢?
肯定是模式串移动的位置不对,模式串是根据 next[j+1] 指示来移动的,说明next[j+1] 计算不对。

回过头再来想象next[j+1]的含义,next[j+1] = k表示模式串j的最大公共子串长度,k代表如果模式串在位置 j+1 发生不匹配时,指针需要回退到位置k。因此,next[j+1]是不是代表最大公共子串的长度不关键,关键是要能指示移动的位置。

再回到上面的问题,如果模式串 t[k+1] = t[j+1],如果在位置 j+1 不匹配,那么在移动后在位置k还是不会匹配:
在这里插入图片描述
既然在位置k不会匹配,那么就需要继续移动,相当于第一次匹配到k时发现不匹配,那么该移动到哪呢? 不就是next[k]吗!所以改进后的算法相当于原来的算法主要改进如下:
如果t[j+1] == t[k+1] , next[j+1] = next[k+1],而不是next[j+1] = k+1。

再把上面的思路整理一下:
(1)模式串 j+1 与目标串 i 不匹配,模式串向右移动,使k = next[j+1] 与 i 对齐
(2)模式串 k 与目标串 i 不匹配,模式串向右移动,使 k’ = next[k] 与i 对齐

实际上,可以看出第一步就是多余的,如果t[j+1] == t[k+1] ,在位置 j +1 发生不匹配时,直接右移动到 next[k] 即可,也就是说 next[j+1] = next [k]

优化后的算法,主要是针对求解next进行了优化,一个核心的变化是求解next[j+1]时,如果t[j+1] == t[k+1] , next[j+1] = next[k+1],而不是next[j+1] = k+1。
附上代码:

void Getnext(int next[],String t)
{
   int j=0,k=-1;
   next[0]=-1;
   while(j<t.length-1) // 循环j,求的是next[j+1]
   {
      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];
   }
}

5、KMP算法整体实现
KMP算法的关键是求next数组,因此如果能够理解next数组的求法,可以说对KMP算法已经掌握十之八九了。求出next数组之后,剩下的就是具体怎么匹配目标串与模式串了,具体匹配的方法前面已经提到过,下面直接附上全部KMP算法

// 目标串为s,模式串为t
int KMP(String s,String t)
{
   int next[MaxSize],i=0;j=0;
   Getnext(t,next);  // 获取next数组
   while(i<s.length&&j<t.length)
   { // 从模式串j = 0 开始匹配
      if(j==-1 || s[i]==t[j]) 
      {
         i++;
         j++;
      }
      else j=next[j];      //j回退。。。若j = -1,说明模式串回退到了起点0
   }
   if(j>=t.length)
       return (i-t.length);         //匹配成功,返回子串的位置
   else
      return (-1);                  //没找到
}

参考文章
【1】KMP算法—终于全部弄懂了https://blog.csdn.net/dark_cy/article/details/88698736

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值