目的
网络上搜索KMP算法的文章和视频很多,五花八门。每个人的理解都有一定不同。next数组有从1开始的,也有从0开始的等等细节,会有点不知所以。导致有的人讲完后,感觉好像懂了,但又没懂。本人刚接触KMP算法也老是没弄清楚,一知半解,过一会就忘了。
最近终于捋清楚思路。所以写下这篇文章,主要以下两个目的。
- 从目前掌握的KMP知识,提供思路和想法。同时加深自己对KMP算法的理解和记忆。
- 让阅读者能够从这篇文章能有所收获。
这里分享下学习心得
首先我们不管KMP的代码是如何实现的,我们先要搞懂原理,原理懂了,实现有差异很正常。实现无非就是下标边界问题。这个根据个人喜好。自行选择。这里不是说代码不重要,代码可以反过来印证原理,加深对原理的理解。
什么是KMP算法
KMP算法是解决查找关键字的问题。给定一个字符串,要在这个文本串中查找特定的字符串,然后返回位置。比较专业的定义参考百度百科 KMP算法。
有人可能会有疑问,为什么要有KMP算法,一句话效率高。
KMP算法原理
首先想下,如果没有KMP算法,要进行字符串查找要如何进行
举例
主串:ababcababcaaaaa
子串(模式串):abacababa
朴素的想法还是很容易想到。暴力求解直接就写两个循环。
- 主串指针mainIdx和子串指针patternIdx同时递进,然后对比。
- 遇到不相等的时候,mainIdx回退到下一个起始位置,patternIdx回退到第一个位置。
这样明显效率很低,算法复杂度为O(n*m)。代码实现略
KMP的算法的主要思路其实就是mainIdx指针不回退,然后patternIdx指针根据next数组来进行移动或者说跳转。(主串指针不动,子串指针移动)。这里的next数组是很关键的一个数组。
我们先从名字上理解next,有下一个的意思。所以这里记住next数组是干什么用的。
- 主串和字串进行逐个对比,
- 在遇到不相等的情况。主串指针不变。那子串的指针如何移动?就是通过查找next数组来进行移动。
引申出next数组如何获取
next数组生成原理
next数组生成只跟子串(模式串)有关。如何生成,原理看下图
蓝色和绿色表示最长相同的前后缀,一步一步直到最后一个字符。
为什么根据next值跳转,patternIdx的移动就是正确的?
数组有两个维度的信息,一个下标,还有一个是下标对应的值。
next数组每一个元素值的含义是如果该位置不相等,则patternIdx指针要跳转到该下表值指定的位置。
发现模式串不相等字符的前面字符串(matchedStr) 是已经匹配过的,
进一步分析这个字符串(matchedStr) 发现,这个字符串(matchedStr) 的最长公共前后缀是不是刚好就是我们对比过的。文字苍白,我们看图
如果理解下面这张图。你就知道为什么要求最长公共前后缀了。
红色的部分已经比较过,所以移动的时候,绿色部分不用再比较了,已经对比过了。(关注:鲁班曰)
KMP算法代码实战
next数组代码实战
大家看了上面的next生成的原理应该还是比较容易理解,那有人就会问看图理解很简单,关键是代码如何生成,代码不会像人类那样靠眼睛一个一个对比查找最长公共最后缀。闲话少说,代码如下:
public static int[] next(String pattern) {
char[] patterns = pattern.toCharArray();
// 默认都是0
int[] next = new int[pattern.length()];
// 匹配过程中 前后缀最长的值,既是长度,也是next数组下标,前缀指针
// 这个字段要理解,是精髓,既是长度也是前缀索引下标
int prefixIdx = 0;
// 后缀下标字符串第二个开始进行计算,第一个默认是0
int suffixIdx = 1;
while (suffixIdx < pattern.length()) {
// 如果相等
if (patterns[prefixIdx] == patterns[suffixIdx]) {
// 长度加1,前缀指针前移(同时也是长度加1)
prefixIdx++;
// 记录当前字串截止的最长公共前后缀长度prefixIdx;
next[suffixIdx] = prefixIdx;
// 后缀指针前移比较下一个
suffixIdx++;
// 这里需要注意,这三句代码的前后顺序不能调换
} else {
// 不相等的情况分两种情况
if (prefixIdx == 0) {
// 如果不相等,记录当前字串截止的最长公共前后缀长度prefixIdx;
next[suffixIdx] = prefixIdx;
// 后缀指针前移下一个
suffixIdx++;
} else {
// (关注:鲁班曰)
// 如果prefixIdx大于0,即长度不为零,
// 精髓所在,好好理解这句代码。
// 这句理解了,你就掌握了kmp
// 首先想想,如果你不这么写,你会这么写。
// 我的理解就是这里把模式串当主串,拿前缀当模式串。有点套娃的意思在里面。
// 想想next是干什么用的
// 主串和字串进行逐个对比,在遇到不相等的情况。主串指针不变。那子串的指针如何移动。
// 就是通过查找next数组来进行移动
// 这里是不是遇到不相等的情况了吗
// 前缀和后缀的字串patterns[prefixIdx] != patterns[suffixIdx]不相等,前缀(模式串)怎么移动
// 既prefixIdx怎么移动,不就是通过查找next数组吗?这里有点绕,多体会以下
// 前缀prefixIdx指针后退,后退的位置根据已知的next得知
// (根据已有的信息,获取下一个prefix的位置和长度,递推思想,根据已有next数组信息推出prefixIdx下一个值)
prefixIdx = next[prefixIdx - 1];
}
}
}
return next;
}
KMP算法完整实战
步骤如下
- 生成next数组
- 主串指针mainIdx和子串指针patternIdx,一起向前进,逐个对比
- 遇到不相等的情况。mainIdx不动。patterIdx如何移动,next告诉你。
- 遇到不相等的情况。如果patterIdx=0。则mainIdx自己加1移动。
package com.ashin.dp;
import java.util.Arrays;
/**
* 这里的next数组的含义,表示跳过几个字符,模式匹配的时候模式串的当前字符不匹配,则该不匹配字符外,剩余字符串的最长公共前后缀是多少,排除到自己本省
*
* @Author: Ash
* @Date: 2020/9/4
* @Description: com.ash
* @Version: 1.0.0
*/
public class KMPExample {
public static void main(String[] args) {
String mainString = "ababaaababababbababadab";
String pattern = "ababa";
int[] next = next(pattern);
System.out.println(Arrays.toString(next));
int mainIdx = 0;
int patternIdx = 0;
while (mainIdx < mainString.length()) {
if (mainString.charAt(mainIdx) == pattern.charAt(patternIdx)) {
mainIdx++;
patternIdx++;
} else if (patternIdx > 0) {
// 精髓所在 (关注:鲁班曰)想想next是干什么用的
int nextIdx = patternIdx - 1;
patternIdx = next[nextIdx];
} else {
mainIdx++;
}
if (patternIdx == pattern.length()) {
// 这里比对完成 直接打印查找的位置
System.out.println(mainIdx - patternIdx);
break;
}
}
}
}
总结
KMP理解后感觉还是很简单的。你不理解,会有人理解,这就是差距。建议多找几篇别人的文章和视频,有的人说的不理解,说不定有哪位大神讲的,你就恍然大悟。反复体会,然后自己实现。相信你很快也能掌握。