KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节

文章目录

  • 1. 前言
  • 2. KMP算法
    • 2.1 KMP算法介绍
      • 2.1.1 Why exit KMP
      • 2.1.2 KMP的思想
      • 3.1.3 KMP部分代码
      • 2.1.4 求next数组
    • 2.2 KMP算法的实现代码
    • 2.3 改进的KMP算法
    • 2.4 小结

1. 前言

本文中我使用的串方式如下图所示:
在这里插入图片描述

  1. 文中的串我都记为s
  2. s[0]不存放字符,从s[1]开始存放字符。
  3. i表达指向主串的指针,j表示指向模式串的指针。

2. KMP算法

2.1 KMP算法介绍

2.1.1 Why exit KMP

传统的BF算法有一个缺点:

  • 当主串的某些子串能部分匹配时,主串中的扫描指针i会回溯的不够精准。当子串能部分匹配的情况比较多时,会导致时间开销增加。

如下图的子串gloglo模式串glogle就是子串能部分匹配的情况:
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第1张图片
从上图可以形象的看出BF算法的缺点:i指针回溯的不够精准。上图中子串匹配中BF算法比理想情况下多匹配了i = 2,3,即多进行了2次不必要的比较。当这种子串部分匹配的情况越多时,这种不必要的比较会更多。

而KMP算法就是针对BF算法的该缺点进行改进的:

  • 当字串和模式串部分匹配时,主串的指针i不需要回溯,只需要调整模式串的指针j = next[j]

注意:细心的同学可能注意到了,KMP算法回溯的指针是j不是i,即是模式串而不是主串,后面会解释。

2.1.2 KMP的思想

首先,我们介绍两个术语:

1. 串的前缀:**包含第一个字符**,且**不包含**最后一个字符
2. 串的后缀:包含最后一个字符,且不包含第一个字符。

	* eg:串s = "abab"
		* s的前缀只有3个:“a”、“ab”、“aba”
		* s的后缀也只有3个:“b”、“ab”、“bab”

		* 注意:此时,我们可以得到串s的最长相同前后缀为"ab"

接下来介绍KMP算法的思想:

假设现在有如下字串部分匹配的情况:
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第2张图片
接下来,我们的处理方式与BF算法不同,由于主串的指针i不能回溯,是j指针回溯,所以,我们需要移动模式串到前面部分相同的位置,由于我们不知道前面部分相同的位置在哪里,我们现在试着一步步移动模式串来达到目的:
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第3张图片
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第4张图片
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第5张图片
此时i指向的代表的非d字符可能是c,从而这个ab????子串可能与模式串相同。

而这整个过程就是在找当前字符前面的所有字符组成的串的最长相同前后缀。即找串s = "abcab"的最大前后缀,为"ab"

那么,如果我们想让此次子串部分匹配的指针精准回溯,我们就应该找当前字符前面的所有字符组成的串的最长相同前后缀。如上图,刚开始时,j = 6,发现是部分匹配,那么j应该被重新赋值为j = 3, 此时3-1 = 2刚好与当前字符前面的所有字符组成的串的最长相同前后缀长度2相等。

注意:上面的3 - 1 = 2中,3需要减1,是因为j需要指向当前字符前面的所有字符组成的串的最长相同前后缀的下一个字符

所以,进一步将问题转换,如果我们发现子串部分匹配时,想让此次子串部分匹配的指针精准回溯,我们应该令j = 当前字符前面的所有字符组成的串的最长相同前后缀的长度 + 1

通常,我们会把模式串的每个字符前面的所有字符组成的串的最长相同前后缀的长度 + 1的值,事先求出来,放到一个next数组中。

比如,上图中模式串的next数组如下图所示:
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第6张图片

说明:其实这里的j = 当前字符前面的所有字符组成的串的最长相同前后缀的长度 + 1中,需要加1是因为我使用的字符串是从s[1]开始,而不是s[0]开始,所以要加1.

3.1.3 KMP部分代码

KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第7张图片
现在,你应该理解了上图的含义。但是目前还没有给出next数组的求法。然而,此时如果此时不考虑next数组里的值是如何求的,我们应该能够实现KMP算法了,具体代码如下:
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第8张图片

注意:解释一下,上面的 j = 0.

  1. 还记得本文中的串,我都是采用的这种格式吧,s[0]不存放字符:
    在这里插入图片描述
  2. 上述中j = 0,是由j = next[1]得到。而next[1],表示在s[1]处,j指针已经指向模式串的第一个字符了,j往前面回溯不了了,所以给next[1]一个不存在的值,通常取0,因为我的s[0]没有字符。
    那么,当j取一个不存在的值,即j = 0时,我们需要让其从新指向s[1].
  3. 也不难想到,j = next[2]只能让回溯s[1]处,所以,next[2]的值必定为1。如果采用我这种串的结构的话。同理,如果是下面这种结构,则必须next[1] = 0
    在这里插入图片描述
  4. 到现在,应该懂了 j = 0,为什么要让他进入 i++,j++中,这样能够让 j = 1,从新指向第一个字符,同时i后移一个。
  5. 那我现在举一个例子,让next[1] = -100,那么算法应该变为:
    KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第9张图片

2.1.4 求next数组

好了,接下来,我们介绍如何手动计算next数组的值:

当第j个字符匹配失败,由前1~j-1个字符组成的串记为S,则:next[j] = S的最长相等前后缀长度 + 1。一般取next[0] = 0,这样就能计算出next数组的值。

下面是一个计算next数组的例子:
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第10张图片

2.2 KMP算法的实现代码

KMP算法的实现比较巧妙,先给出实现,代码代码如下图所示:
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第11张图片
上述代码的思想,还是使用的我们之前介绍的思想。求next数组的代码也是求最长相同前后缀的思想,下面我解释一下get_next()函数。

KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第12张图片
那么问题来了,为何k = next[k],就能找到长度更短的相同前缀后缀呢?,可以利用对称性解释:
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第13张图片
至此,我们就完成了对KMP算法的学习。还剩下一个问题,就是之前我说的,为什么精准回溯模式串的j指针,而不是主串的i指针?

其实,答案也很简单,你可以看到,我们对模式串的精准回溯是需要求next数组才能实现,如果是精准移动主串的i指针,那么我们对于每一个子串都需要求next数组,这样时间的开销基本没变,有时反而更费时间。而模式串是固定的,我们只需要求一次next数组就可以了。

不得不说,有时候等价转换能将一些不能解决的事务解决,KMP算法就是将i指针回溯等价转换成了j指针回溯,从而解决了问题。

2.3 改进的KMP算法

你以为之前的KMP算法就是最优的吗?其实,KMP还有一种特殊情况会导致j指针回溯还是不够精准。这种情况就是:s[j] != s[i]; m = j; j = next[j]; 当s[m] == s[j]时,明显此时必定存在s[m] != s[i],这样,我们完全可以这样:
KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第14张图片
具体做法就是,遍历next数组

  1. 如果s[j] = s[next[j]], 则nextval[j] = nextval[next[j]],
  2. 否则nextval[j] = next[j]

2.4 小结

KMP算法你真的懂了吗?详细讲解KMP算法,理解每一个细节_第15张图片

你可能感兴趣的:(数据结构,算法)