KMP算法是什么,我就不在此赘述了。如下的文章主要记录了我在学习KMP算法中的一些重点内容,包括next数组该怎么求解,该如何用代码来实现求解next算法的这一过程。对于一些不好理解的地方,我找了一些图片,自己也绘制了相应的图示便于理解。
如果完全不了解KMP算法,可以先参见:
-
阮一峰老师的这篇文章:字符串匹配的KMP算法
-
英语好的小伙伴可以直接阅读这篇文章:The Knuth-Morris-Pratt Algorithm in my own words
本文参考:如何借助BM算法轻松理解KMP算法?
KMP算法初探
当模式串与主串发生不匹配的时候,我们试着一次性多滑动几位。而不是又从头开始比较。比如下面的e、c
发生不匹配了。
凭借肉眼,我们决定应该这么移动,然后主串和模式串继续开始比较。
对应到算法中,我们无法在程序中也这样滑动模式串
去进行比较。而是用两个指针去进行比较。
比如这样:
或者是这样:
然后,在继续下面的内容之前,需要先了解一下前缀子串和后缀子串
求解next数组
用代码实现KMP算法的时候,我们不可能像凭借肉眼的观察那样,得知到发生失配时,应该从头开始比较,还是从某个位置继续开始比较。
因此我们需要借助一个next数组,来告诉程序当发生失配时,应该怎么做。
那next[i]究竟表示什么呢?
当在模式串的index = i的位置与主串发生了失配时,就可以跳转到next[index - 1] + 1的位置开始继续比较,而不是从头开始。
求next数组。计算模式串每个前缀的最长可匹配前缀。思路:列举出该子串的所有前缀子串和所有后缀子串,从中找出最长的公共子串。比如下图所示,找到了ababa这个前缀子串的最长可匹配前缀为aba
。
举例:
如果在这之后发生了失配,就可以将模式串一次性移动5 - length(aba
) = 2格,再与主串进行比较。
对应到程序代码里,我们应该这样进行移动,将j指针进行回溯,而不是滑动模式串。
当出现失配时,比如上图,我们已经成功匹配了ababac
但是在模式串的c
的这个位置,却匹配失败了。我们已经计算出了ababa
的最长可以匹配前缀长度为3,也就是aba
这个字符串,然后指针j回溯5 - 3 = 2
个单位。
然后干脆一点,我们用一个next[]数组,直接指示当在index = i
这个位置发生失配时,我们查阅next[index - 1]的值来计算应该将j指针回溯到什么位置。比如这里,当index = 5发生失配时,查阅next[ 5 -1] = next[4],next[4]应该等于3。但是一般以-1作为开始,故这个地方习惯写成2。所以next[4] = 2。当在index = 5这个位置发生失配时,查阅next[index - 1]的值将其加一,得到的就是j应该回溯到的位置。
如下是手工计算next的一个过程:
计算ababacd
的next数组
说明:
对于a
这个前缀子串而言:其没有前缀,也无后缀,所以最长可匹配前缀也就无从谈起。若在第一个字符这里发生了失配。只能从头开始比较。
对于ab
这个前缀子串而言:其前缀子串为a
,后缀子串为b
,无公共元素。若在第二个字符这里发生了失配。也只能从头开始比较。
aba
的公共子串为a,发生失配时,从a的一个元素b继续开始比较
计算出来了next[]数组,又该怎么使用呢?
如果模式串在index = j
的位置发生了失配
,则查看next[j - 1]
的值,通过这个值可以来判断是否需要重新开始比较。
继续看:
程序如何求得next数组?
有了next数组以后,当发生失配时,我们知道该怎么做。可见:关键在于next[]数组的计算,我们在程序中如果也需要去一个个的列举每个子串的前缀字串和后缀子串,然后通过比较找出其中最长的子串,如果是这样,时间复杂度就会非常高。
注意:如下所说的子串b[]也就是指模式串。
这段话比较难以理解,的好好读读。
-
如果 next[i-1]=k-1,也就是说,子串 b[0, k-1]是 b[0, i-1]的最长可匹配前缀子串。如果子串 b[0, k-1]的下一个字符 b[k],与 b[0, i-1]的下一个字符 b[i]匹配,那子串 b[0, k]就是 b[0, i]的最长可匹配前缀子串。所以,next[i]等于 k。
-
如果 b[0, k-1]的下一字符 b[k]跟 b[0, i-1]的下一个字符 b[i]不相等。
我们假设 b[0, i]的最长可匹配后缀子串是 b[r, i]。如果我们把最后一个字符去掉,那 b[r, i-1]肯定是 b[0, i-1]的可匹配后缀子串,但不一定是最长可匹配后缀子串。所以,既然 b[0, i-1]最长可匹配后缀子串对应的模式串的前缀子串的下一个字符并不等于 b[i],那么我们就可以考察 b[0, i-1]的次长可匹配后缀子串 b[x, i-1]对应的可匹配前缀子串 b[0, i-1-x]的下一个字符 b[i-x]是否等于 b[i]。如果等于,那 b[x, i]就是 b[0, i]的最长可匹配后缀子串。如何求得 b[0, i-1]的次长可匹配后缀子串呢?次长可匹配后缀子串肯定被包含在最长可匹配后缀子串中,而最长可匹配后缀子串又对应最长可匹配前缀子串 b[0, y]。于是,查找 b[0, i-1]的次长可匹配后缀子串,这个问题就变成,查找 b[0, y]的最长匹配后缀子串的问题了。
Java代码实现KMP算法
public class KMP {
public static int kmp(String mainStr, String subString) {
if (mainStr == null || subString == null || mainStr.length() == 0 || subString.length() == 0) {
return -1;
}
char[] main = mainStr.toCharArray();
char[] sub = subString.toCharArray();
int[] next = getNext(sub);
int j = 0;
for (int i = 0; i < mainStr.length(); ++i) {
while (j > 0 && main[i] != sub[j]) {
j = next[j] + 1;
}
if (main[i] == sub[j]) {
++j;
}
//如果j==sub.length,说明模式串已经完全匹配上了,返回下标
if (j == sub.length) {
return i - j + 1;
}
}
return -1;
}
private static int[] getNext(char[] sub) {
int[] next = new int[sub.length];
next[0] = -1;
int k = -1;
for (int i = 1; i < sub.length; ++i) {
while (k != -1 && sub[k + 1] != sub[i]) {
k = next[k];
}
if (sub[k + 1] == sub[i]) {
++k;
}
next[i] = k;
}
return next;
}
}
模式串与主串匹配成功以后,返回的是在主串中第一次匹配成功时的起始下标。
如下所示: