KMP算法详解
KMP(Knuth-Morris-Pratt)算法是一种用于在大串中寻找小串的字符串匹配算法。它通过在字符串匹配过程中避免不必要的重复比较,显著提高了效率。KMP算法的核心思想是利用字符串中已经匹配的部分信息来优化匹配过程,减少回溯操作。
KMP算法的基本思想
KMP算法的关键是使用"部分匹配表"(也叫做"前缀表"或者"失配函数"),该表记录了每个子串的前缀与后缀的相似信息。通过这个表,可以在字符匹配失败时,跳过一些无意义的比较,直接跳到下一个可能匹配的位置。
1. 前缀和后缀的定义
- 前缀:一个字符串的前缀是它的一个子串,且不包括字符串的最后一个字符。例如,字符串 "ABCD" 的前缀有 ""、"A"、"AB"、"ABC"。
- 后缀:一个字符串的后缀是它的一个子串,且不包括字符串的第一个字符。例如,字符串 "ABCD" 的后缀有 "BCD"、"CD"、"D"。
在KMP算法中,我们需要关注的是前缀 和后缀的重合部分。特别是,对于模式串中的每个位置,我们需要知道它的最大前缀和后缀的匹配长度。
2. 部分匹配表(前缀表)
部分匹配表(也叫"失配函数")记录了模式串中每个位置的前缀和后缀的最长匹配长度。具体来说,前缀表的第 i
个值表示的是模式串从位置 0
到 i-1
的子串的最长前缀后缀匹配长度。
例如,对于模式串 ABABAC
,它的部分匹配表为:
java
索引: 0 1 2 3 4 5
模式串: A B A B A C
前缀表: 0 0 1 2 3 0
解释:
A
没有前缀和后缀匹配,因此为0
。AB
没有前缀和后缀匹配,因此为0
。ABA
有前缀和后缀A
匹配,因此为1
。ABAB
有前缀和后缀AB
匹配,因此为2
。ABABA
有前缀和后缀ABA
匹配,因此为3
。ABABAC
没有前缀和后缀匹配,因此为0
。
3. 构建部分匹配表
为了有效实现KMP算法,需要首先构建部分匹配表。我们从模式串的第二个字符开始,逐个计算出每个位置的最长前缀后缀匹配长度。
构建过程:
- 初始化一个数组
prefixTable
,大小为模式串的长度,初始值为0
。 - 使用两个指针:一个指向模式串的当前字符(
i
),一个指向前缀长度(j
)。 - 遍历模式串,对于每个字符,如果它与前一个字符匹配,则
prefixTable[i] = j + 1
,否则通过prefixTable[j-1]
来跳过一些字符,避免重复计算。
4. KMP匹配过程
- 初始化两个指针,一个指向文本串
T
(大串),一个指向模式串P
(小串)。 - 比较模式串中的字符与文本串中的字符:
- 如果匹配,则继续比较下一个字符。
- 如果不匹配,则根据部分匹配表跳到模式串中的一个位置,避免了从头开始重新匹配。
5. KMP匹配的时间复杂度
- 构建部分匹配表的时间复杂度为
O(m)
,其中m
是模式串的长度。 - 匹配过程的时间复杂度为
O(n)
,其中n
是文本串的长度。 - 因此,KMP算法的总时间复杂度为
O(m + n)
,相比传统的暴力匹配算法O(m * n)
,大大提高了效率。
例题:
6. KMP算法的实现
java
public class KMP {
// 构建部分匹配表
public static int[] buildPrefixTable(String pattern) {
int m = pattern.length();
int[] prefixTable = new int[m];
int j = 0; // 前缀长度
for (int i = 1; i < m; i++) {
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
j = prefixTable[j - 1];
}
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
prefixTable[i] = j;
}
return prefixTable;
}
// KMP字符串匹配
public static int kmpSearch(String text, String pattern) {
int[] prefixTable = buildPrefixTable(pattern);
int n = text.length();
int m = pattern.length();
int j = 0; // 模式串的指针
for (int i = 0; i < n; i++) {
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = prefixTable[j - 1]; // 跳到前缀表的位置
}
if (text.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == m) {
return i - m + 1; // 匹配成功,返回匹配的起始位置
}
}
return -1; // 匹配失败
}
public static void main(String[] args) {
String text = "ABABDABACDABABCABAB";
String pattern = "ABABCABAB";
int result = kmpSearch(text, pattern);
System.out.println("Pattern found at index: " + result); // 输出: Pattern found at index: 10
}
}
7. 总结
- KMP算法通过构建前缀表来优化字符串匹配的效率,避免了暴力匹配中的重复计算。
- 在字符串匹配过程中,利用已经匹配的部分信息来跳过不必要的比较,节省时间。
- 相比传统的暴力匹配算法,KMP算法的时间复杂度大大降低,是高效的字符串匹配算法。