KMP算法:从理论到代码
KMP算法:从理论到代码
一、理论
1. 什么是KMP算法
KMP 算法,全称 Knuth-Morris-Pratt 算法,是由 Donald Knuth、James H. Morris 和 Vaughan Pratt 三位计算机科学家于 1977 年共同提出的一种高效的字符串匹配算法。它用于在一个文本串(Text)中查找一个模式串(Pattern)出现的位置,匹配过程中利用了已部分匹配的信息,避免了重复比较,从而大大提高了匹配效率。
2. 什么是字符串匹配
在开始之前,先了解一下 字符串匹配 的问题:
-
目的:在一段文本(Text)中查找一个特定的子串(Pattern)。
-
举例:在 "hello world" 中查找 "world",目标是找到 "world" 在 "hello world" 中的位置。
3. 朴素的字符串匹配方法
最直接的想法是:
- 从文本的第一个字符开始,与模式串的第一个字符比较。
- 如果匹配,就继续比较下一个字符。
- 如果全部字符都匹配成功,说明找到了模式串。
- 如果中途有字符不匹配,就从文本的下一个字符开始,重复上述步骤。
问题在于:当文本很长,而模式串较短,且包含重复字符时,效率会很低,因为需要大量的重复比较。
4. KMP 算法的基本思想
KMP 算法的核心是 利用模式串本身的规律,避免重复比较。
- 当遇到不匹配时,不需要回到文本串的下一个字符,而是利用已经匹配的信息,跳过一些不必要的字符。
- 这通过 部分匹配表(next 数组) 实现。
5. 理解前缀和后缀
在构建部分匹配表之前,需要理解 前缀 和 后缀 的概念:
- 前缀:不包含最后一个字符的所有以第一个字符开始的连续子串。
- 后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串。
举例:对于字符串 "ABCDAB":
- 前缀包括:"A"、"AB"、"ABC"、"ABCD"、"ABCDA"
- 后缀包括:"BCDAB"、"CDAB"、"DAB"、"AB"、"B"
6. 构建部分匹配表(next 数组)
目的:记录模式串中每个位置之前的最长可匹配前缀后缀的长度。
步骤:
- 初始化一个与模式串长度相同的数组,初始值为 0。
- 从模式串的第二个字符开始,依次计算每个位置的部分匹配值。
举例:模式串 "ABCDABD"
位置索引(从 0 开始) | 字符 | 部分匹配值(next 值) |
---|---|---|
0 | A | 0 |
1 | B | 0 |
2 | C | 0 |
3 | D | 0 |
4 | A | 1 |
5 | B | 2 |
6 | D | 0 |
计算过程:
- 位置 0:只有一个字符 'A',部分匹配值为 0。
- 位置 1:前缀 'A',后缀 'B',不匹配,部分匹配值为 0。
- 位置 2:前缀有 'A'、'AB',后缀有 'BC'、'C',不匹配,部分匹配值为 0。
- 位置 3:同理,部分匹配值为 0。
- 位置 4:
- 前缀有:'A'、'AB'、'ABC'、'ABCD'
- 后缀有:'BCDA'、'CDA'、'DA'、'A'
- 前缀 'A' 与后缀 'A' 匹配,部分匹配值为 1。
- 位置 5:
- 前缀有:'A'、'AB'、'ABC'、'ABCD'、'ABCDA'
- 后缀有:'BCDAB'、'CDAB'、'DAB'、'AB'、'B'
- 前缀 'AB' 与后缀 'AB' 匹配,部分匹配值为 2。
- 位置 6:没有匹配,部分匹配值为 0。
7. KMP 算法匹配过程
利用 next 数组,在匹配失败时,调整模式串的位置,而不回退文本串的指针。
步骤:
- 初始化文本串指针
i=0
,模式串指针j=0
。 - 比较
Text[i]
与Pattern[j]
:- 如果匹配,
i++
,j++
。 - 如果不匹配:
- 如果
j > 0
,令j = next[j - 1]
,不移动i
,利用 next 数组跳过一部分字符。 - 如果
j == 0
,i++
,从文本串的下一个字符继续匹配。
- 如果
- 如果匹配,
- 当
j
达到模式串的长度,表示匹配成功。
8. 举个例子
文本串: "ABABDABACDABABCABAB"
模式串: "ABABCABAB"
next 数组:通过计算得到 [0, 0, 1, 2, 0, 1, 2, 3, 2]
匹配过程:
i=0
,j=0
,比较 'A' 与 'A',匹配,i++
,j++
。i=1
,j=1
,比较 'B' 与 'B',匹配,i++
,j++
。i=2
,j=2
,比较 'A' 与 'A',匹配,i++
,j++
。i=3
,j=3
,比较 'B' 与 'B',匹配,i++
,j++
。i=4
,j=4
,比较 'D' 与 'C',不匹配。j > 0
,令j = next[3] = 2
,i
不变(回退模式串的位置)。
j=2
,比较 'D' 与 'A',不匹配。j > 0
,令j = next[1] = 0
,i
不变。
j=0
,比较 'D' 与 'A',不匹配。j == 0
,令i++
,i=5
。
- 重复上述步骤,直到
j
达到模式串的长度,匹配成功,或i
超过文本串长度,匹配失败。
二、代码
KMP算法的Java实现
public class KMPAlgorithm {
// 构建 next 数组
public static int[] buildNext(String pattern) {
int m = pattern.length();
int[] next = new int[m];
int j = 0; // 已匹配的前缀长度
for (int i = 1; i < m; i++) {
// 当出现不匹配,且 j > 0 时,回退到前一个可能匹配的位置
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
// 如果字符匹配,已匹配的前缀长度加 1
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
// KMP 搜索算法
public static int kmpSearch(String text, String pattern) {
int n = text.length();
int m = pattern.length();
int[] next = buildNext(pattern);
int j = 0; // 模式串指针
for (int i = 0; i < n; i++) {
// 出现不匹配,且 j > 0 时,按照 next 数组回退
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
// 如果字符匹配,模式串指针加 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);
if (result == -1) {
System.out.println("未找到匹配的模式串。");
} else {
System.out.println("模式串出现在位置:" + result);
}
}
}
解释:
- buildNext 方法用于构建 next 数组。
- kmpSearch 方法用于在文本串中查找模式串,返回匹配的起始位置。
三、总结
- KMP 算法 通过借助模式串的部分匹配信息,避免重复比较,大大提高了匹配效率。
- next 数组 是 KMP 算法的核心,它记录了模式串中每个位置之前的最长可匹配前缀后缀的长度。
- 学习建议:
- 多画图、多手算,理解 next 数组的构建过程。
- 跟踪算法的匹配过程,加深理解。
- 亲自实现一遍代码,调试运行,巩固所学。
四、常见问题解答
- 为什么要使用 next 数组?
- 因为当出现匹配失败时,next 数组告诉我们模式串中有多少字符是重复的,可以跳过这些重复的部分,减少比较次数。
- KMP 算法的时间复杂度是多少?
- KMP 算法的时间复杂度为 O(n),其中 n 是文本串的长度。
- 学习 KMP 算法的难点是什么?
- 主要在于理解 next 数组的构建和应用,以及在匹配失败时如何利用 next 数组调整模式串的位置。
分类:
标签:
KMP算法
版权申明
本文系作者 @卸了磨的驴 原创发布在KMP算法:从理论到代码。未经许可,禁止转载。
全部评论