KMP算法难理解的地方在哪里?
我认为有两点
- 滑动规则
- next数组的构建
滑动规则
下面对KMP算法滑动情形进行分析:
有两个下标很重要:
i对应主串中当前正在进行比较的元素下标
j对应模式串中当前正在进行比较的元素下标
1. 一开始初始化i j都为0
2. 遍历主串中的元素 也就是i逐渐+1 直到找到第一个和模式串首位字符匹配的a[i]
在代码中就是这样体现的:
for (int i = 0; i < n; ++i) {
if (a[i] == b[j]) {
++j ; //找到第一个匹配的字符之后j++变为1
}
}
找到第一个匹配的字符j++变为1 当前进行比较的模式串中的元素变为第二个字符
同时j>0 再进行下一轮主串遍历的时候就满足进入while循环的前置条件
while (j > 0 && a[i] != b[j]) {
j = next[j - 1] + 1;
}
3. 下一步需要找到第一个不匹配的字符才能进行while循环
如果暂时没找到不匹配字符的话 j 会一直+1也就是模式串和主串从模式串的[0,j]一直保持着匹配状态
我们称这段匹配的部分为好前缀
一旦找到第一个不匹配的字符也就是满足a[i] != b[j]那么 j 就是模式串中第一个 "叛乱者"
我们称他为坏字符之前匹配的部分[0,j-1]就是好前缀
4. 进入while循环 , 这一步就是最最关键的部分,KMP算法怎么实现一下滑动好几位 他的规则是什么
while (j > 0 && a[i] != b[j]) {
j = next[j - 1] + 1;
}
我们只需要拿好前缀本身,在它的后缀子串中,查找最长的那个可以跟好前缀的前缀子串匹配的。假设最长的可匹配的那部分前缀子串是 {v} ,长度是 k。我们把模式串一次性往后滑动 j-k 位,相当于,每次遇到坏字符的时候,我们就把 j 更新为 k,i 不变,然后继续比较。
这里有两个地方需要解释一下:
-
滑动位数怎么计算出是
j-k的?
其实是(j-1) - (k-1)也就是好前缀中匹配的前后缀子串的屁股下标之差 , 在图中就是两个a对应为下标 什么 -
j 更新为k是什么意思?
j 有两个作用 :
第一个是在上面提到的 作为坏字符的下标 这一点是对于模式串而言的 ,当j 更新为k的时候 坏字符对应的位置由原来的 j变成了k ,而k正好是好前缀中最长前后可匹配子串的长度 所以坏字符位置实际上就变成了好前缀的最长可匹配前缀子串的后面一位也就是图中aba后面的 b
下需要从这个前缀子串的后面进行匹配 在下面的图中也就是比较b和e是否相等
第二个作用是 j 在while循环中的移动可以看成模式串相对于主串在移动, j 位置的字符换成向前移动j-k个位置的字符, 相当于模式串向后移动j-k个位置。至于现在 j 的字符小标计算方法就是 j-(j-k) 也就是 j=k 。
注意点: j在两个位置会发生移动 一个是在while循环中
while (j > 0 && a[i] != b[j]) {
j = next[j - 1] + 1;
}
另一个是在for循环中
for (int i = 0; i < n; ++i) {
if (a[i] == b[j]) {
++j ; //找到第一个匹配的字符之后j++变为1
}
}
当找到主串和模式串中连续匹配的部分也就是所谓的好前缀的时候 ,j在for循环中会连续++直到找到一个坏字符
我的理解中把这个过程称为一次完全匹配尝试也就是说这个过程的目的是想尝试完全匹配模式串中的所有字符 但是中途杀出了一个不速之客(坏字符)导致这次尝试没能达到最终目的
但是这次尝试保留了有价值的部分 这个部分可以保留给后人 再下一次完全匹配尝试中不需要像第一次完全匹配尝试那样从模式串的头字符开始进行匹配
这个部分是好前缀吗??? 并不是.... 这个部分是好前缀中最长可匹配前缀子串
为了表述起来方便,我把好前缀的所有后缀子串中,最长的可匹配前缀子串的那个后缀子串,叫作最长可匹配后缀子串;对应的前缀子串,叫作最长可匹配前缀子串。
为什么是这个部分呢?
因为这个部分 (下图中红色部分) 可以和上一次尝试得到的好前缀的后半部分进行匹配 看坏字符和最长可匹配前缀子串后面的一个字符 (下图中紫色区域的第一个字符) 是否匹配
如果匹配的话就跳出while循环 接着进入for循环看模式串的下面的字符和主串中下面的字符是否匹配
如果不匹配的话就将红色部分视作新的好前缀 (下图中一整根长条表示一个好前缀) 接下去寻找这个好前缀的最长可匹配前缀子串 如此循环直至好前缀不存在可匹配前后缀子串 比如变成一个字符 或者ab这样的好前缀
举个例子:
abaaba 作为好前缀,此时j=7, 红色部分(为了简化最长可匹配前缀的称呼)是aba 拿aba后面的a和ababaa后面的一位坏字符做比较发现不匹配就接着循环 下一步
aba作为好前缀 j变成4 , a是红色部分 拿a后面的b和abaaba后面的一位坏字符做比较发现不匹配接着循环 下一步
a作为好前缀 j变成 1 不存在红色部分
这样的话 j又会变回0 (为什么? 因为next数组设定了不存在红色部分的值就为-1 再+1就变成了0) 也就是说 匹配的部分清零了所有的尝试全部作废
5.接下去只能东山再起 回到第一步 再次遍历主串找和模式串中第一个字符相等的字符
完整代码如下
// a, b分别是主串和模式串;n, m分别是主串和模式串的长度。
public static int kmp(char[] a, int n, char[] b, int m) {
int[] next = getNexts(b, m);
int j = 0;
for (int i = 0; i < n; ++i) {
while (j > 0 && a[i] != b[j]) { // 一直找到a[i]和b[j]
j = next[j - 1] + 1;
}
if (a[i] == b[j]) {
++j;
}
if (j == m) { // 找到匹配模式串的了
return i - m + 1;
}
}
return -1;
}