难哭我的KMP算法及代码

目录

(不好意思,由于部分公式,图片找不到了,详细讲解可前往公众号 突然的灵感的啊 查看)
闲话少聊

说实话,本作者(灵感)也是搞了好几个小时才明白了KMP算法,这中间的心酸和无奈可想而知,明白之后就立即写下这千字长文来帮助大家更快,更深的理解,希望大家看完本文后能够有一个清晰透彻的理解。
不得不说的是,使用KMP算法的题只能算作简单题目,可见现在的孩子都卷成什么样了(害怕),再不努力就真成渣渣了。
提醒:想要透彻理解KMP算法,可能需要多看几遍,明白之后你将不得不说:“ 牛逼 ”!

重难点

重点:

KMP算法的核心要义

KMP为什么这么牛?

前缀函数和匹配模式

难点:

移动的距离怎么计算,为什么要移动这么多?

前缀函数的值计算怎么这么难懂?

为什么要把前缀函数的值赋给 j ?

入门知识

KMP算法是模式匹配的一种常用算法,即对子串在主串中定位,得到子串在主串中出现的第一个位置,它是由Knuth,Pratt,Morris共同创造(向大佬致敬!),此算法的时间复杂度仅为O(n+m),而暴力解法最坏的情况需要O(n*m),可以见得此算法的厉害了吧!
此算法的核心要义即为 不需要像暴力解法那样一个一个地去匹配,而是跳过一段不必要的距离(这些中间字符即使去匹配也不会成功)来进行匹配。

(不好意思,由于图片找不到了,详细讲解可前往公众号

KMP算法的基本移动逻辑,主串中的字符不动,而时模式串中的字符移动一定的距离,来与其进行匹配,其实我们可以观察出,在第一次匹配时,前两个字符成功,第三个失败,我们可以直接跳到第四个字符进行匹配,图中的二,三步完全是不必要的。而此算法正是和我们想的一样这么去做的。

接下来,把你的疑问写到纸上,我将一一给你解答!

前后缀

前缀函数记作 pi(i)
举个例子:

有一段字符串 a a b a a a b

首先第一个字符 a 没有真前缀和真后缀,根据规定为0(可以发现对于任意字符串 pi (0)=0 必定成立)

pi(1) = 1,因为 aa 最长的一对相等的真前后缀为 aa,长度为 1;

pi(2) = 0,因为 aab没有对应真前缀和真后缀,根据规定为 0;

pi(3) = 1,因为 aaba 最长的一对相等的真前后缀为 a,长度为 1;

pi(4) = 2,因为 aabaa 最长的一对相等的真前后缀为 aa,长度为 2;

pi(5) = 2,因为 aabaaa 最长的一对相等的真前后缀为 aa,长度为 2;

pi(6) = 3,因为 aabaaab 最长的一对相等的真前后缀为 aab,长度为 3。

从上述例子来看,前后缀的意思我想你应该已经理解大部分了,这里我再补充一点:
上述字符串的前缀集合为 { a, aa, aab, aaba, aabaa, aabaaa}
类似地写出:后缀集合为 { b, ab, aab, aaab, baaab, abaaab}

黑体的即为此串的最大的公共前后缀(理解此步很重要,对后续求前缀函数值 pi (i)起到了支持,理解next函数定义公式,根据公式计算函数值非常重要)

初级理论

在主串与子串(又叫模式串)从头到尾的匹配过程中,我们不可能保证一次匹配成功,因此,当主串中的第 i 个字符和模式串的第 j 个字符失配(即不相等)时,模式串应该往后跳多少个字符,跳到哪个字符然后和它进行匹配呢?
下面我们来讲解一下:
假设主串的第i个字符此时应该和模式串中的第k个字符进行比较,那么证明前面的字符已经匹配成功,则有:

(不好意思,由于公式找不到了,详细讲解可前往公众号查看)

若模式串中存在满足上式的两个子串,那么在匹配时,当主串的第i个字符和模式串中的第j个字符比较不等,仅需要将模式串向右滑动至模式串的第k个字符和主串的第i个字符对齐,再进行比较。

有的同学要问:为什么要这样呢?下面咱们详细解释一下(请看下面的释疑解难部分)

释疑解难1

01
为什么要移动这些距离,移动到k?
答:满足上述等式的模式串前后 k 个字符对应相等,而主串的第 i 个字符前面的 k 个字符与模式串的后 k 个字符(第 j 字符前面)已经匹配成功,只是主串的第 i 个字符与模式串的第 j 个字符没有匹配成功

在这里插入图片描述

而模式串的前后 k 个字符是对应相等的,(换句话说就是前面的 k 个字符在最后面又按原来的顺序出现了一次),既然主串的第 i 个字符前面的 k 个字符与模式串的后 k 个字符(第 j 字符前面)可以匹配成功那么证明和模式串前面的 k 个字符也可以匹配成功,那么我们就不需要再次进行匹配了,直接让它移动到需要匹配的位置不就好了吗。
在这里插入图片描述

即使去匹配,因为主串中的第 i 个字符前面的 k 个字符与模式串的前 k 个字符的不对应,还是会匹配不成功的,所以我们干脆直接让可以匹配的对应起来(即移动一段距离),匹配后面的 k 个字符,如果后面的匹配成功了,那么就找出了子串在主串中的位置,如果不成功继续去做同一件事。
在这里插入图片描述
如果你还没有大彻大悟的话,那么本灵感将不得不遗憾地告诉你:
在这里插入图片描述

深入计算

此部分涉及公式复杂,还是请到微信公众号查看

释疑解难2

01
前缀函数值和真前后缀数有何相同和不同?
答:上面说过,任意字符串的第一个字符的真前后缀数为0,前缀函数公式规定:第一个字符的前缀函数值为0,这点是一样的。

那么区别在哪?在这:
前缀函数值是计算当前字符前面的一串字符的最大公共真前后缀值,使这个值等于 k-1,然后算出 k 值即为所求的最大前缀函数值,是不包括自己的,而真前后缀值的计算是包括当前字符本身的。
第二个区别就是,如果前面的字符没有真前后缀值那么就为1,符合公式的第三种情况。
为什么要这么计算呢?
有人会说你 k - 1 = 最大公共真前后缀值,可以直接计算包括当前字符的最大公共真前后缀值呀,那我告诉你,你举得例子不过是巧合罢了,如果是下面的例子,第二个计算你就错了。
接下来讲一下为什么在当前字符的前面找?
1.仔细观察公式你会发现最后一个是 ,也就是说,j 是当前字符,在 j 前面找两个有最大相等数的子串,这就是为什么不能包括当前字符的原因了。
为什么要使k - 1 = 最大公共真前后缀值呢?
2.再仔细观察公式,最大子串的最后一个字符 的下标是 k - 1,代表在第 k 个字符前有最大相等数的子串,此子串的长度 k - 1即为第 j 个字符前的最大公共真前后缀值。因此使它们相等,由最大公共真前后缀得出最大的 k 值。

如果前面的你觉得还行,那么本灵感要告诉你的是:
在这里插入图片描述

代码剖析

Void get_pi(SString T, int pi[])
{  // 先使 j, i 指向模式串的第1,2个字符,对自己进行匹配
   i = 1; j = 0; pi[1] = 0;
   // 令 pi[1] = 0;是严老师认为 j 是从1开始的,同上面的实例
   
   while ( i <= T[0] )
   {  // 这里 i <= T[0] ,T[0]是串的长度
   // 只有第一次会不用判断就 pi(2) = 1;
   // 第二个字符前面只有一个字符,因此是1
      if ( j == 0 || T[i] == T[j] )
      // 这里其实是相当于两个相同的模式串在匹配,
      // *目的是求出移动的距离即移动到哪个位置*
      {
         ++i; ++j;
         pi(i) = j;
         // 如果匹配则pi(i) = j; pi(i)里存入的是后一个字符不匹配时,j 应该移动的位置
         // 下面详细讲解,会有点难
      }
      else
         j = pi(i);
         // 上面存入的是下一个不匹配时, j 应该返回的位置,那这里不匹配自然就会把刚才的位置传给 j.
   }
   
}
int Index_KMP(SString S, SString T)
{  // 求解模式串S在主串T中的出现的位置
 // 两个串都从第一个字符开始
   i = 0; j = 0;
   while ( i <= T[0] || j <= S[0] )
   {
      if ( j == 0 || T[i] == S[j] )
      {
      ++i; ++j;
      }
      else
         j = pi(j);
   }
   // 匹配成功,i = j 位置都是模式串的最后一个字符
   // 不过 i 在主串上且比 j 大,减去模式串的长度,即为模式串在主串上的位置
   if ( j > T[0] )
      return i - T[0];
   else
   // 没找到
      return 0;
}

清楚了代码的意义之后,我们来深度剖析一下难点
在这里插入图片描述

释疑解难3

01
为什么先要对自己进行匹配,求得函数的值呢?
答:首先我们在失配时要移动到的位置还不知道,所以要计算一下,手动计算方法前面已经讲的很清楚了,(有人说我讲的和课本上不一样,我只能说表面上不一样,为了方便你理解,我只不过按照公式讲的,如果真的按课本分情况,你很难明白,当然,如果你想学习课本,可以直接联系我,我单独讲,这里不多说)

接下来讲为什么代码要这样写
前两个字符的值就不多说了,关键是为什么要把 j 赋给pi,我来告诉你,赋值前面的条件是两个字符相等,因此,在下一个字符匹配情况未知时,我们将最后匹配成功的字符序号记录到 pi 数组中,也就是函数值。
02 为什么要把 pi 的值赋给 j ?
答:有了上面的解答,这块就很容易理解了,失配时,我们将 j 返回到原来最后匹配成功的字符序号就行了,让它再去匹配。

最后

好了,以上就是本人尽力总结出的解析,如果你还没有搞懂,请恕本人不才,懵懂的同学有什么问题可以尽情在主页发送消息留言,欢迎大家一起讨论,

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值