简述
KMP(Knuth-Morris-Pratt)算法是一种用于在文本字符串中查找模式(子字符串)位置的高效字符串匹配算法。与传统的暴力匹配算法相比,KMP算法通过利用已经匹配过的信息来避免重复比较,从而提高了匹配效率。
核心思想
想要更好的理解KMP算法,我们需要先对几个核心概念有个了解
- 前缀:文本中从第一个字母开始的子串
- 后缀:文本中以最后一个字母结尾的子串
- 模式Pattern:需要进行的查找的Pattern
知道概念之后,我们可以利用前后缀,来计算模式P的部分匹配表(LPS array)
计算方法: 比如,对于模式串 ABABC,其部分匹配表(记为 next[] 数组)如下:
- 对于模式串中的第一个字符 A,没有前缀和后缀,因此 next[0] = 0。
- 对于模式串 AB,没有相同的前缀和后缀,因此 next[1] = 0。
- 对于模式串 ABA,最大相同的前缀和后缀是 A,长度为 1,因此 next[2] = 1。
- 对于模式串 ABAB,最大相同的前缀和后缀是 AB,长度为 2,因此 next[3] = 2。
- 对于模式串 ABABC,没有相同的前缀和后缀,因此 next[4] = 0。
- 最终得到部分匹配表 next[] = [0, 0, 1, 2, 0]。
那么问题来了,计算这个匹配表有什么用呢?
它可以帮助在模式中的匹配失败时快速跳转到下一个可能的匹配位置,而无需从头开始重新匹配!!!!
举个例子,假设我们有以下场景:
- 文本串 text = “ABABABCABABABCABABABC”
- 模式串 pattern = “ABABC”
- next[] = [0, 0, 1, 2, 0]
逐字符比较过程:
ABABABCABABABCABABABC
ABABC
A 对应 A,匹配。
B 对应 B,匹配。
A 对应 A,匹配。
B 对应 B,匹配。
C 对应 A,不匹配。
很容易理解,如果我们暴力法的话,此时就需要从第二个字符,从头开始继续比较,效率很低。
而有了匹配数组后,当我们发现pattern[4] 不匹配时, 直接寻找next[4-1] = 2, 我们能直接从第三个字符开始比较,也就是下面的情况
ABABABCABABABCABABABC
ABABC
此时继续
A 对应 A,匹配。
B 对应 B,匹配。
C 对应 C,匹配。
结束。
此时如果不匹配,则继续从 next[2-1],开始比较,以此类推
从上面的例子可以看出,KMP能大大加快我们匹配时移动的速度,解决了字符串匹配的效率问题。
下面是java代码示例
public class KMPAlgorithm {
// 生成部分匹配表
public static int[] buildNext(String pattern) {
int m = pattern.length();
int[] next = new int[m];
next[0] = 0;
int j = 0;
for (int i = 1; i < m; i++) {
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
// KMP匹配算法
public static List<Integer> kmpSearch(String text, String pattern) {
int n = text.length();
int m = pattern.length();
int[] next = buildNext(pattern);
int j = 0;
List<Integer> result = Lists.newArrayList();
// 注意一个从0开始,这个算法类似上面,只不过j == m 时表示完全匹配了, 上面的方法从1开始不可能j = m
for (int i = 0; i < n; i++) {
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
if (text.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == m) {
result.add(i - m + 1);
j = next[j - 1];
}
}
return result;
}
public static void main(String[] args) {
String text = "ABABDABACDABABCABAB";
String pattern = "ABABCABAB";
int index = kmpSearch(text, pattern);
System.out.println("Pattern found at index: " + index);
}
}
补充:扩展KMP算法
在经典的 KMP 算法基础上发展而来的一种算法。它不仅用于字符串模式匹配,还可以用来快速计算两个字符串的最长公共前缀(Longest Common Prefix,LCP)
- Z 函数:描述了字符串中某个位置开始的后缀与字符串本身前缀的匹配长度,适用于字符串匹配问题。