在翻看了大量的CSDN、bilibili、blog等之后,发现关于KMP的博客是真的多,但是大多数对KMP的讲解都不是很清晰,当然我这篇也不是很清晰,但是关于KMP我确实做了比较详细的了解与阐述。
相信你能看到这篇文章,说明你已经知道KMP是因何而生的了,在此处我就不做过多的介绍了。
直接进入正题。
KMP算法
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。
KMP算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了一定程度的提高。
想要搞明白KMP算法,首先需要直到前缀和后缀。
前缀
比如,有一个字符串abbaab,那么它的前缀有:a、ab、abb、abba、abbaa
后缀
同上字符串,它的后缀有:b、ab、aab、baab、bbaab
公共前后缀
公共前后缀指的是在一个字符串中一个字串即是前缀也是后缀。
前缀表
前缀表代表的是模式串i位置及之前的字串中的最长公共前后缀(包括i位置),其作用是当i位置发生不匹配时,可以根据前缀表来回溯。
比如字符串ababa,
i == 0,字串为“a”,最长公共前后缀长度为0
i == 1,字串为“ab”,最长公共前后缀长度为0
i == 2,字串为“aba”,最长公共前后缀长度为1(a)
i == 3,字串为“abab”,最长公共前后缀长度为2(ab)
i == 4,字串为“ababa”,最长公共前后缀长度为3(aba)
但是,在很多实际应用中,前缀表(prefix)为了适应代码,出现了两种变形(next)。
第一种
将前缀表整体右移,第一位补-1.
第二种
将前缀表整体减一。
具体如下表,方式1代表初始的前缀表,方式2代表整体右移的前缀表,方式3代表整体减一的前缀表
构建next数组
有了数组的前缀和后缀之后,我们就可以构建next数组了。根据要构建的next数组形式有不同的构建方式。
推荐(无广告,只是再查看了无数视频资料之后找到的讲解的比较好的分享一下):www.bilibili.com/video/BV1UL…
方式1
这种求出来的数组被叫做前缀表(prefix),也可以用作next,看个人习惯。
在该方法中next数组的含义是模式串 {0,i}区间的字符串的最长公共前后缀。
求前缀和的过程(本人的思考过程)
在以下过程中next[i]数组代表的是i位置上的最长前后缀长度,str[]数组代表模式串转换成的字符数组。
由于在 i = 0 时,子串只有一个字符,前后缀都为空,所以规定next[0] = 0。
在求next数组时,我们需要比较子串的前缀和后缀,然后给数组的当前位置赋值,给数组赋值,我们只需要找个变量循环++即可,但是如何比较子串的前缀和后缀是我们需要解决的问题。
比较子串的前缀和后缀
子串区间:【0,i】,
由上可知,前缀必定以str[0]开头,后缀必定以str[i]结束,且next[i-1]代表的是【0,i-1】区间的最长公共前后缀长度。next[i-1]的取值又有两种情况。
情况1:如果next[i-1] > 0则说明【0,i - 1】区间有公共的前后缀,我们设定preLen代表 next[i-1]。
str[preLen]即可代表【0,preLen】区间最长公共前后缀的前缀的下一个字符。
举个例子,模式串为
abcdabca,我们可以手算出其next数组为{0,0,0,0,1,2,3,1}当 i=6 时,next[i-1] = next[5] = 2 = preLen,str[preLen] = str[2] = ‘c’。
str[preLen] 代表的就是【0,5】区间,即
abcdab的最长公共前后缀(ab)的前缀的下一个字符。
此时我们只需要判断str[i]和str[preLen]是否相等
- 相等,那么
next[i] = next[i-1] + 1 = preLen + 1。 - 不相等,那么我们需要对preLen回退,让
preLen = next[preLen - 1]。
为什么让preLen = next[preLen - 1]?
举个:chestnut:
模式串
abababc,手算next[] = {0,0,1,2,3,4,0}当
i = 6时,根据过程已经得到j = 4,此时子串为ababa。
根据图可知,回退一次之后,
str[i] != str[j]仍然成立,继续回退,此时
j = next[1] = 0,不符合j > 0的条件,退出循环。
情况2:如果next[i-1] <= 0,则说明【0,i-1】区间没有公共的前后缀,此时我们只需要判断str[i]和str[preLen]是否相等
- 相等,那么
next[i] = next[i-1] + 1 = preLen + 1 - 不相等,
next[i] = 0 = preLen,因为preLen不会小于0,只会大于等于0。
至于preLen为啥不会小于0,因为在给preLen赋值时,在它等于0时就会退出
方式1的代码如下:
public int[] getPrefix(String pattern) {
int len = pattern.length();
int[] prefix = new int[len];
for (int i = 1, j = 0; i < len; i++) {
// i 指向next,j 指向前缀
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
// j 回退 ,
j = prefix[j - 1];
}
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
prefix[i] = j;
}
return prefix;
}
一个完整的过程如下:
方式2
求方式2的next数组就是求从0到发生不匹配位置之前的模式子串的最长的公共前后缀的长度。
注意,此时next数组的含义发生了改变,此时next数组的含义是{0,i-1}区间最长的公共前后缀的长度。
比如,主串为ababacaacb,模式串为ababaa。
第一次匹配时,发生不匹配的位置如下:
此时的模式子串为:ababa,该字串的前缀有:a、ab、aba、abab,后缀有:a、ba、aba、baba。
所以,公共前后缀有:a、aba,最长公共前后缀就是:aba,所以next[5]=3。
举个:chestnut:.
对下面模式串求next数组:
求方式2的流程跟方式1是类似的,在此处不再赘述(因为过程口述起来实在是比较麻烦)。
方式2的代码为
public int[] getNext(String pattern) {
int len = pattern.length();
if (len == 1) {
// 如果模式串长度为 1,直接返回即可
return new int[]{-1};
}
char[] chars = pattern.toCharArray();
int[] next = new int[len];
next[0] = -1;
next[1] = 0;
//i = next 待赋值位置 , j = next[i - 1] = 0,代表{0,i-2}区间的最长公共前后缀长度
int i = 2, j = 0;
while (i < len) {
if (chars[i - 1] == chars[j]) {
// 判断chars[j]和chars[i-1]是否相等,相等的话,最大的公共前后缀长度+1,直接给next[i]赋值
// 例:aab,
next[i++] = ++j;
} else if (j > 0) {
// j > 0, j回滚
j = next[j];
} else {
// j = 0,不能继续回滚了,此时只能设置next为0
next[i++] = 0;
}
}
return next;
}
方式3
求出方式1后,循环-1即可。直接求的方法,还没有研究,但是代码如下(没有详细验证):
public void getNextM3(String needle) {
// len >= 2
char[] chars = needle.toCharArray();
int[] next = new int[chars.length];
next[0] = -1;
int k = -1;
for (int i = 1; i < chars.length; ++i) {
while (k != -1 && chars[k + 1] != chars[i]) {
k = next[k];
}
if (chars[k + 1] == chars[i]) {
++k;
}
next[i] = k;
}
}
好了,此时关于KMP的我个人的理解已经结束了,如果在看完此篇文章还有疑惑的话,建议多思考一下,想想如果这个问题摆在自己面前,自己该怎么做,然后再看代码是如何写的,作者又为什么这么写,然后就是多练习了,关于KMP,其实其思想没有很难,难的是将其用代码实现。这很考验我的逻辑思考能力。
希望你能有所收获,另外,如发现本文存在任何问题,随时欢迎指出。