1、基本概念
KMP算法是一种高效的字符串匹配算法,算法名取自三位共同发明人的名字首字母
应用场景:子串在主串中的定位。比如求子串在主串中出现的起始位置,子串在主串中出现的次数等等。
2、算法核心
2.1:暴力求解
定义主串指针i(初始化0), 子串指针j , 不断比较主串和子串每个字符是否一致,如果一致,继续比较,如果不一样,主串指针变成1,子串指针变成0,从新开始比较。可以看出,整个算法时间复杂度为O(m*n),效率是比较低的。
2.2, KMP算法
KMP其实是对上面的暴力求解进行了改进,利用的是匹配失败之前,匹配成功的信息。原则是保持主串的指针i 位置不回退。
如图,匹配失败之后,保持指针i 不变,尽可能移动子串的指针到有效的匹配位置(这个意思是指,i前面两位已经匹配成功了,不需要额外进行比较了。此时只需要比较 i 和 j 两个位置的字符就可以了。)
进一步解释:
- 如图 子串 index=5 的时候,匹配失败,但是前面 5位是已经匹配成功了的(已知信息)。
- KMP算法固定主串指针i 不回退,那么很明显就需要变更子串的指针j。如何知道j 要变到哪个位置呢?如果变成0,看图中的情况肯定是不正确的,这是因为子串前5位中,存在前缀AB 和后缀AB,这两个是一摸一样的,下次匹配可以跳过AB的匹配,这样可以减少两次对比。
- 因此,当子串index=j 匹配失败时,我们只需要知道 index = j-1 位置相同的最长的前后缀长度,就可以找到指针 j 需要回退的位置了。
通过上面的分析,其实问题可以先简化为 求 子串 对应index坐标 最长前后缀的长度即可。网上一般叫做求 next数组。
求next数组的思路可以参考如下博客
https://blog.csdn.net/qq_45910820/article/details/134877075
下面给出相关的代码。其实求next数组,可以看做是两个相同的字符串自己和自己比较。
/**
* 模板字符串的next数组:
* next数组每个值的含义:当前字符之前的字符串中,最大相同前缀后缀的长度
* 比如 ababaca ---》 [0, 0, 1, 2, 3, 0, 1]
*/
public static int[] getNext(String str) {
int length = str.length();
int[] next = new int[length];
// 初始化第一位的长度为0;
next[0] = 0;
// j代表最大前缀长度,初始化为0,这里一起作为指针辅助遍历。
int j = 0;
// 这里有两个指针,i 和 j ,其中i 是不断往后遍历,j会涉及到回退
// j 始终是要比 i 小的,因为求某个坐标的前后缀长度,要小与前面字符串的长度,否则就没有意愿了。
byte[] bytes = str.getBytes();
for (int i = 1; i < length; i++) {
// 字符不一样
while (j > 0 && bytes[i] != bytes[j]) {
// 这一步要好好理解一下。
j = next[j - 1];
}
// 字符一样,说明相同的前后缀长度加1
if (bytes[i] == bytes[j]) {
j++;
}
// 将长度赋值给当前坐标
next[i] = j;
}
return next;
}
public static int KMP(String str, String sub) {
if (sub.isEmpty()) {
return 0;
}
// 获取next数组
int[] next = getNext(sub);
// 定义指向子串的指针
int j = 0;
for (int i = 0; i < str.getBytes().length; i++) {
// 如果当前位置字符串不匹配,利用最长前缀表跳过已经匹配成功的部分
while (j > 0 && str.charAt(i) != sub.charAt(j)) {
j = next[j - 1];
}
// 如果当前位置匹配,那么就继续向后匹配
if (str.charAt(i) == sub.charAt(j)) {
j++;
}
// 如果子串匹配完成了,那就返回起始位置的坐标。
if (j == sub.length()) {
return i - sub.length() + 1;
}
}
// 子串没有匹配成功,返回-1
return -1;
}