KMP是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母
几个关键的概念
- 前缀表(prefix table):记录某些信息的表
- next数组:KMP算法的前缀表(专门用来记录有关回退匹配信息的表)
如何计算前缀表
- 长度为前1个字符的子串
a,最长相同前后缀的长度为0 - 长度为前2个字符的子串
aa,最长相同前后缀的长度为1(a与a重复) - 长度为前3个字符的子串
aab,最长相同前后缀的长度为0(a!=b) - 长度为前4个字符的子串
aaba,最长相同前后缀的长度为1(a=a) - 长度为前5个字符的子串
aabaa,最长相同前后缀的长度为2(aa=aa) - 长度为前6个字符的子串
aabaaf,最长相同前后缀的长度为0(b!=f)

模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀
如何使用前缀表(next数组)

这个就是next数组的作用->如果是之前的话,这里匹配失败后主串和子串都需要回去重新来进行匹配,但是基于next数组,我们可以少匹配很多,而不是O(M*N)的时间复杂度->主串一直在向右进行,而子串则是在不匹配的时候进行一定程度上的回滚(如下图)

如果这里的图解释的不够清楚的话,看【最浅显易懂的 KMP 算法讲解】https://www.bilibili.com/video/BV1AY4y157yL?vd_source=29280a6402e27c696a32363167333156的动图讲解吧
next数组的生成原理
#include <vector>
#include <string>
using namespace std;
void getNext(int* next, const string& s) {
// 1. 初始化
int i = 0; // i代表前缀长度(也指向前缀的下一个字符)
int j = 1; // j代表后缀末尾位置(从1开始,因为next[0]固定为0)
next[0] = 0; // 初始化第一个位置
// 2. 循环构建
while(j < s.size()) {
if(s[i] == s[j]) {
// --- 匹配成功的情况 ---
i++; // 前缀长度增加
next[j] = i;// 记录当前后缀位置j对应的最长公共前后缀长度
j++; // 后缀指针向后移,处理下一个字符
}
else {
// --- 匹配失败的情况 ---
if (i > 0) {
// 核心:如果i>0,说明前面还有匹配,根据next数组回退
// 注意:这里不要 j++,回退后要用新的 s[i] 和当前的 s[j] 再比一次
i = next[i-1];
} else {
// i已经回退到0了,还是不匹配
next[j] = 0; // 认命,长度为0
j++; // 只能去处理下一个字符了
}
}
}
}
这里还需要注意一下,我的这种next数组的生成方式是PMT(Partial Match Table)的,有的教材会基于PMT进行一定程度上的改变,比如0->-1、0->1这种左右移动
这是生成next数组的方法,下面来一步步的讲讲为什么
(注意这里的i和j和我代码里面有点区别,我这里表述的是从1开始的)
-
如果当前的长度为2,如果左边的i+1处和右边的j+1处的值相同,那么next[j+1]不就等于next[j]+1了么,等于了的话,那么就记录当前的i值到对应的next数组的位置上去

-
但是后来再走一点点的话,因为匹配 C 失败了,我们不能拥有长度为 4 的公共前后缀。我们退而求其次,看看能不能找到更短一点的

-
因为匹配失败了因此我们就需要回退
因为大前缀 ABA 的尾巴是 A,大后缀 ABA 的尾巴也是 A
结论:我们可以确信,当前字符串的结尾一定有一个 A 是匹配开头的 A 的(第一个字符和最后一个字符都是A这是一定的) -
因此我们不需要重比第一个 A,直接试图去匹配第二个字符(我们将“期望匹配长度”从 3 降级(回退)到 next[3-1](即 next[2])的值,也就是 1)→然后现在我们就要继续进行匹配了,左边的i+1处的B和右边的j+1处的B匹配成功了,因此next就等于i+1(1+1=2)

注意清楚了呀,这里的i从原来的3->4进发失败了被回退到了3->1,因此左边只有一个A,所以这里进行匹配判断的是两个B
当然这里还有一个例子:
状态:
字符串: a a b a a f
下标: 0 1 2 3 4 5
^ ^ ^
j=2 (前缀结尾) i=5 (后缀结尾)
比较 s[i] ('f') 和 s[j] ('b') -> 不等!我们需要缩小 j。
查 next[j-1] 即 next[1]。
next[1] 对应 "aa",它的 next 值是 1。
这意味着:虽然 "aab" 匹配不上,但前面的 "aa" 里还有一个长度为 1 的公共前后缀 "a"。所以 j 变成 1。
再次比较 s[i] ('f') 和 s[j] (s[1]即'a') -> 还是不等!
继续回退:j = next[j-1] = next[0] = 0,j 变成了 0。
比较 s[i] ('f') 和 s[0] ('a') -> 还是不等!j 已经是 0 了,没法退了。所以 next[5] = 0。
1万+

被折叠的 条评论
为什么被折叠?



