KMP 算法理论及实现 (Java)
一、简介
KMP 算法是一种字符串匹配算法。核心思想是利用匹配失败之后的信息,尽量减少模式串与主串的匹配次数来达到快速匹配的目的。理应一个 next() 函数实现,该函数本身包含了模式串的局部匹配信息。KMP 算法的时间复杂度是 O(m+n)
二、问题引入
有一个目标串 S ,和一个模式串 P,现在要寻找模式串 P 是否在目标串 S 中以及出现的位置
三、暴力算法
不做介绍
四、KMP 算法
1. 算法思想介绍
- 以下图所示主串及模式串为例
- 依次向下对比,直到出现不匹配的情况
- 通过观察可知模式串在已经匹配的字符串内有公共前后缀,取最长公共前后缀
- 而后,直接移动模式串,使得前缀移动到后缀的位置
-
- 注:这样使前缀直接移动到后缀的方式是成立的,可以证明中间没有匹配的情况 (后文有证明)
- 再次对比,发现不匹配。这里由于选取的前后缀内容是相同的,所以当前缀到达后缀的位置后,只需要对指针当前位置及以后进行对比即可,指针前面的前缀不需要再次对比
- 再次寻找最长公共前后缀
- 移动模式串
- 再次对比,发现匹配成功
2. 模式串移动证明
- 以下图所示主串及模式串为例
-
- 上图中,模式串的最长前后缀为 A,并且匹配到箭头所指位置发现不匹配,即箭头之前是完全匹配的
- 现假设在移动前缀到后缀位置的过程中遗漏了一种匹配的情况,如下图所示 模式串2(模式串2 和 模式串 相同)
- 因为 模式串2 在此处与主串相匹配,并且假设 模式串2 的第一个 A 与主串的第二个 A 之间的部分称为 B 部分,由于 模式串2 与主串匹配,故可以确定 模式串2 的一部分内容
- 又因为模式串在箭头所指之前与主串完全匹配,所以可以推出模式串部分内容
- 又因为 模式串2 和 模式串相同,所以,更新模式串的内容如下
- 显然,最长公共前后缀变成了
ABA
,与开始的假设最长公共前后缀为 A
矛盾。 - 所以
将模式串最长前缀移动到最长后缀位置时中间可能会有遗漏的匹配的情况
命题不成立
3. 确定模式串移动距离
- 由前文 “算法思想介绍” 已知,模式串匹配时向后移动的距离只取决于模式串自己,所以可以对模式串单独分析,建立一个
next[]
数组记录在模式串对应的元素不匹配时向后移动多少距离 - 以下图所示模式串为例
- 建立对应的
next[]
数组
-
- 如图,
next[i]
表示在0 ~ i
这个字串的最长公共前后缀的长度。如,next[3]
表示ABAA
这个字串的公共前后缀的长度,显然是 1
- 如图,
next[]
数组的含义- 当
模式串[i]
与主串发生不匹配时,就将模式串[next[i-1]]
移动到当前位,再进行比较。比如,如果模式串[5]
与主串发生不匹配,由于next[4] = 2
,所以将模式串[2]
,即 A 拉到当前位再与主串相比较 - 也就是说,如果失配在
模式串[i]
,那么模式串[i] ~ 模式串[i-1]
这一段里,前next[i-1]
个字符恰好和后next[i-1]
个字符相等,也就是说可以拿长度为next[i-1]
的那一段前缀来顶替当前字串的后缀的位置,让匹配继续
- 当
4. 快速构建 next[ ] 数组
快速构建next[]
数组的核心思想为: 模式串自己与自己做匹配。即, 使用递推的方式求解next[]
数组
- 情况一:当前后缀的下一位与当前前缀的下一位相同
-
- 以上图所示模式串为例,已知
next[3] = 1
,即最长公共前后缀长度为 1 ,即 A,此时要求next[4]
,由于0 ~ 3
这一字串的最长后缀向后加一为 AB,最长前缀向后加一也为 AB,所以next[4] = next[3] + 1 = 2
- 以上图所示模式串为例,已知
- 情况二:当前后缀的下一位与当前前缀的下一位不同
- 以下图所示模式串为例
-
- 图中,
next[12] = 5
,字串的最长公共前后缀为ABCAB
,但是,在使用递推的方式求next[13]
的时候发现,最长后缀加一位和最长前缀加一位之后并不相同- 记
模式串[0] ~ 模式串[12]
的公共前后缀为L0
,并且作为前缀的部分记为L0+
,作为后缀的部分记为L0-
,模式串[0] ~ 模式串[13]
的公共前后缀为L1
- 此时,L0 向后扩大的方向走不通,但是我们的目的是
通过 L0 求得 L1
。 - 已知此时 L0 扩大时不匹配,所以 L1 一定要比 L0 短。即,只看前缀的话,L1 一定落在 L0+ 内。
- 由于我们的目的依然是
通过 L0 求得 L1
,所以我们可以在当前 L0 的范围内,缩短 L0 的长度,再通过向后扩大一位 L0 而后进行比较的方式来求得 L1 。 - 但是,如何得知 L0 缩小的长度呢
- 观察 L0 ,已知 L0+ 和 L0- 相等,但是 L0+ 和 L0- 也存在自己的前后缀(尽管它们是对应相同的),当 L0+ 存在 前缀 和 L0- 的 后缀 相同时(即 L0 有公共的前后缀),此时,如上图所示,由于 L0 有公共前后缀 L00+ L00-,所以将指针 P0 移动到 2 的位置,即 C,再进行比较(
i = next[i-1]
)
- 记
- 图中,
-
-
- 因为目的是
通过 L0 求得 L1
,L1+ 和 L1- 是相同的,所以在一个值匹配不上的时候,先找到这个值前面已经被匹配的部分,即 L0 的公共前后缀,这样的部分可能有多个,在找到这个值前面已经被匹配到的部分之后,再进行对这个值的匹配,如果还是不成功,就重复以上步骤,如果所有的公共前后缀都是用完,仍然没有成功,那么这个值对应的 next[ ] 数组置零
- 因为目的是
-
5. 算法实现(Java)
package KMP算法;/*
*author:yangyu
*creation time:2023/6/8 23:32
*/
public class Kmp {
public static void getNext(int[] next, String sub) {
next[0] = -1;
if (sub.length() == 1) {
// 当子串只有一个数据的时候,next数组的长度为1
return;
}
// 前提条件是数组长度大于1
next[1] = 0;
int k = 0;
int j = 2;
while (j < sub.length()) {
if (k == -1 || sub.charAt(j - 1) == sub.charAt(k)) {
next[j] = k + 1;
j++;
k++;
} else {
k = next[k];
}
}
}
public static int KMP(String str, String sub, int pos) {
// 判断两个串不能为空
if (str == null || sub == null) {
return -1;
}
int i = pos;// i遍历主串 从pos位置开始
int j = 0; // j遍历字串 从0开始
int strLength = str.length();
int subLength = sub.length();
if (strLength == 0 || subLength == 0) {
return -1;
}
// 判断pos位置合法性
if (pos < 0 || pos > strLength) {
return -1;
}
//求字串的next数组
int[] next = new int[subLength];
getNext(next, sub);
while (i < strLength && j < subLength) {
if (j == -1 || str.charAt(i) == sub.charAt(j)) {
i++;
j++;
} else {
j = next[j];
}
}
if (j == subLength) {
// 字串遍历完之后 j应该等于sublength
// 找到返回字串在主串中的起始位置
return i - j;
} else {
// 找不到返回-1
return -1;
}
}
public static void main(String[] args) {
String str = "ababcabcdabcdefg";
String sub = "aabaabb";
int pos = KMP(str, sub, 0);
System.out.println(pos);
}
}