算法背景
字符串匹配是计算机科学中最基础的问题之一:给定一个文本串 text 和一个模式串 pattern,找出 pattern 在 text 中所有出现的位置。暴力解法的时间复杂度为 O(m×n),而 KMP(Knuth-Morris-Pratt)算法通过构建前缀函数(Prefix Function),将匹配过程优化到 O(m+n) 的线性时间,是字符串算法的经典之作。
核心思想
KMP 的核心在于利用已匹配的信息避免回退。当匹配失败时,模式串不需要从头开始比较,而是根据前缀函数跳转到最长相同前缀后缀的位置继续匹配。
前缀函数 pi[i] 定义为:子串 pattern[0..i] 的最长相等真前缀与真后缀的长度。例如对于模式串 "ababc",pi 数组为 [0,0,1,2,0]。
完整Java实现
public class KMPMatcher {
/**
* 计算前缀函数(π数组)
* @param pattern 模式串
* @return 前缀函数数组
*/
public static int[] computePrefixFunction(String pattern) {
int m = pattern.length();
int[] pi = new int[m];
// pi[0] 恒为0,从i=1开始计算
for (int i = 1; i < m; i++) {
int j = pi[i - 1]; // j表示前一个位置的最长前缀后缀长度
// 当当前字符不匹配时,回退到更短的前缀
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
j = pi[j - 1];
}
// 如果匹配成功,长度加1
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
pi[i] = j;
}
return pi;
}
/**
* KMP字符串匹配
* @param text 文本串
* @param pattern 模式串
* @return 所有匹配起始位置的列表
*/
public static java.util.List<Integer> kmpSearch(String text, String pattern) {
java.util.List<Integer> result = new java.util.ArrayList<>();
if (pattern.isEmpty() || text.length() < pattern.length()) {
return result;
}
int[] pi = computePrefixFunction(pattern);
int j = 0; // pattern中的当前匹配位置
for (int i = 0; i < text.length(); i++) {
// 不匹配时回退
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = pi[j - 1];
}
// 匹配成功
if (text.charAt(i) == pattern.charAt(j)) {
j++;
}
// 完整匹配模式串
if (j == pattern.length()) {
result.add(i - pattern.length() + 1);
j = pi[j - 1]; // 继续寻找下一个匹配
}
}
return result;
}
/**
* 统计模式串在文本串中的出现次数(允许重叠)
*/
public static int countOccurrences(String text, String pattern) {
return kmpSearch(text, pattern).size();
}
public static void main(String[] args) {
String text = "ababcabcabababd";
String pattern = "ababd";
System.out.println("文本串: " + text);
System.out.println("模式串: " + pattern);
int[] pi = computePrefixFunction(pattern);
System.out.print("前缀函数: ");
for (int v : pi) System.out.print(v + " ");
System.out.println();
java.util.List<Integer> matches = kmpSearch(text, pattern);
System.out.println("匹配位置: " + matches);
System.out.println("出现次数: " + matches.size());
// 测试重叠匹配
String text2 = "aaaa";
String pattern2 = "aa";
System.out.println("\\n重叠测试 - 文本: " + text2 + ", 模式: " + pattern2);
System.out.println("匹配位置: " + kmpSearch(text2, pattern2));
}
}
复杂度分析
- 前缀函数计算:每个位置
i最多被访问两次(一次作为当前位置,一次被回退),时间复杂度 O(m),空间复杂度 O(m)。 - 匹配过程:同理,文本串每个位置最多被访问两次,时间复杂度 O(n)。
- 总体复杂度:O(m+n) 时间,O(m) 额外空间。
应用场景
KMP算法广泛应用于文本编辑器查找、DNA序列比对、网络数据包过滤、日志分析等需要高效字符串匹配的场景。理解KMP的前缀函数思想,也是掌握AC自动机、Z函数等高级字符串算法的基础。