KMP算法(Knuth-Morris-Pratt算法)是字符串匹配领域的重要突破,它通过预处理模式串构建next数组,将传统暴力匹配的O(mn)时间复杂度优化至O(m+n)。这个算法最精妙之处在于它利用了模式串自身的结构特性,避免了主串指针的回退。
理解KMP的关键在于掌握"部分匹配"的概念:当某个字符匹配失败时,我们已经知道前面部分字符是匹配成功的,这些信息不应该被浪费。
举个例子,假设我们在主串"ABABABC"中查找模式串"ABABC":
next数组的每个元素next[i]表示模式串前i+1个字符组成的子串中,最长的相等真前缀和真后缀的长度。这里的"真"指的是不包括字符串本身。
例如模式串"ABABC"的next数组:
构建next数组的核心是使用双指针技术:
具体实现中有几个关键点需要注意:
java复制public static int[] getNextArr(String src) {
if (src.length() == 0) return null;
if (src.length() == 1) return new int[1];
int[] next = new int[src.length()];
int left = 0, right = 1;
while (right < src.length()) {
if (src.charAt(left) == src.charAt(right)) {
next[right] = left + 1;
right++;
left++;
} else if (left > 0) {
left = next[left - 1]; // 关键回退操作
} else {
next[right] = 0;
right++;
}
}
return next;
}
虽然看起来有双重循环(while循环和内部的if-else),但实际上时间复杂度是O(m),其中m是模式串长度。这是因为:
空间复杂度显然是O(m),用于存储next数组。
KMP匹配过程同样采用双指针策略:
匹配过程可以分为三种情况处理:
java复制public static ArrayList<Integer> getIndexAll(String tar, String src, int[] next) {
ArrayList<Integer> index = new ArrayList<>();
if (tar.length() < src.length()) return null;
int tarIndex = 0, srcIndex = 0;
while (tarIndex < tar.length()) {
while (tarIndex < tar.length() && srcIndex < src.length()) {
if (tar.charAt(tarIndex) == src.charAt(srcIndex)) {
tarIndex++;
srcIndex++;
} else if (srcIndex > 0) {
srcIndex = next[srcIndex - 1]; // 关键跳转
} else {
tarIndex++;
}
}
if (srcIndex == src.length()) {
index.add(tarIndex + 1 - src.length());
srcIndex = next[srcIndex - 1]; // 继续寻找下一个匹配
}
}
return index;
}
KMP算法的正确性基于两个关键观察:
当模式串在位置j匹配失败时,next[j-1]告诉我们前j-1个字符的最长公共前后缀长度k,这意味着:
类似next数组构建,KMP匹配的时间复杂度是O(n+m):
空间复杂度主要是O(m)的next数组空间。
对于洛谷P3375这样的题目,数据规模可能达到10^6级别,普通的Scanner输入会导致超时。必须使用BufferedReader进行优化:
java复制BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String tar = br.readLine();
String src = br.readLine();
输出同样需要优化,使用StringBuilder批量构建输出结果:
java复制StringBuilder sb = new StringBuilder();
for (int pos : index) {
sb.append(pos).append("\n");
}
for (int i : next) {
sb.append(i).append(" ");
}
System.out.print(sb);
在实际编码中,需要特别注意以下边界情况:
在实现KMP算法时,开发者常犯的错误包括:
调试时可以:
标准next数组在某些情况下可以进一步优化。例如模式串"AAAAAB"在匹配失败时,可以跳过连续的'A'直接回退到第一个非'A'的位置。这种优化称为nextval数组。
现代文本编辑器的查找功能通常采用Boyer-Moore算法,但在某些特定场景下KMP仍有优势,特别是:
KMP算法在DNA序列匹配中有重要应用,特别是:
暴力匹配算法(朴素算法)在最坏情况下需要O(mn)时间复杂度,而KMP保证O(m+n)。实际中:
Boyer-Moore算法通常比KMP更快,因为它:
但KMP在某些情况下仍更适用:
Rabin-Karp使用哈希技术:
在实现KMP算法时,我总结了以下几点经验:
一个常见的性能陷阱是频繁的字符串charAt操作,在Java中可以考虑先将字符串转为字符数组:
java复制char[] srcArr = src.toCharArray();
char[] tarArr = tar.toCharArray();
这样可以减少方法调用开销,对于超大规模数据有一定优化效果。