本文讲解了个人在跟随b站up主 董晓算法 学习KMP算法中碰到的疑惑。原视频链接:http://【F03【模板】KMP 算法】 https://www.bilibili.com/video/BV1Ag411o7US/?share_source=copy_web&vd_source=e8efef5ed935b98788b215fdb8024747
next数组的构建无疑是KMP算法的难点,本人的疑惑大多都是在试图理解如何构建next数组的过程中产生的。
一、关于j = 0的必要性。
j初始化为0是为了方便 回跳 和 对ne[i]进行赋值(此模板的ne[i]是指在模板串中第1位到第i位的最长相等前后缀)如果j初始化为1(此处指同时修改构造时的逻辑保证数组能正常构造),在回跳过程中需使j = ne[ j - 1 ],在逻辑上不太方便理解(在我看来,正推是比逆推简单的)。
二、关于回跳的思想形成
回跳是为了节省ne[]数组的构建时间,使用KMP算法的目的就是降低时间复杂度,能节约时间的解法无疑更优。能回跳是因为在之前的判断中符合前后缀相等,而回跳的原因是因为j+1位于第i位不符合,所以可以回到可匹配的前缀位置,再对前缀的下一位和第i位进行比较。(这段看不懂的可以看图解)
上图描述的是结束了 i = 8 的循环的状态,在进行i = 9的循环时,我们发现 j + 1与 i 不相同:
难道要让 j = 0,重新计算吗? 显然,重新计算太浪费时间了,让我们来观察一下,我们可以发现上图框起来的两个字母是等效的
再往前推,我们还发现了一个等效的a。既然中间的a不符合 j+1 = i 的要求,我们就可以回跳至之前的a判断。根据递推的思想,我们回跳至的等效位置一定是最靠近的一个等效位置,所以这样判断就可以节约一定的时间。
在解决完next数组的构成后,KMP算法就很简单了。
完整代码如下:欢迎大家在评论区讨论
#include<iostream>
using namespace std;
char model[1000] = { 0 };
char str[1000] = { 0 };
int lenMod, lenStr;
int ne[1000] = { 0 };
void getNext() {//创建next数组
int i, j;//双指针对模式串进行遍历,j为前缀,i为后缀
ne[1] = 0;
for (i = 2, j = 0; i <= lenMod; i++) {
//j初始化为0是为了方便 回跳 和 对ne[i]进行赋值,此模板的ne[i]是指在模板串中第1位到第i位的最长相等前后缀
while (j != 0 && model[i] != model[j + 1]) {//不匹配则回跳至上一段最长的相等前后缀,直到可再次进行比较或回到0位
//能回跳是因为在之前的判断中符合前后缀相等,而回跳的原因是因为j+1位于第i位不符合,所以可以回到可匹配的前缀位置,再对前缀的下一位和第i位进行比较
j = ne[j];
}
if (model[j + 1] == model[i]) {//当i向后移一位时,最长相等前后缀最多+1
j++;
}
ne[i] = j;//找到最长相等前后缀之后便用ne[i]进行记录
}//完成了next数组的创建
}
int main(void)
{
int i, j;
cin >> str + 1 >> model + 1;//保证下标与字符串位置对应
lenStr = strlen(str + 1);
lenMod = strlen(model + 1);
getNext();
for (i = 1, j = 0; i <= lenStr; i++) {//i扫描主串,j扫描模式串
while (j != 0 && str[i] != model[j + 1]) {//不匹配则回跳
j = ne[j];
}
if (str[i] == model[j + 1]) {
j++;//匹配则判断下一位
}
if (j == lenMod) {
cout << i - lenMod + 1 << endl;
j = ne[j];//防止主串中含多个模式串导致j>lenMod
}
}
return 0;
}