KMP 算法是一种高效的字符串匹配算法,用于在一个主文本串(
text)中查找一个模式串(pattern)是否出现,如果出现,返回其首次出现的起始位置。
1. 问题背景
假设我们要在字符串 text = "ABABDABACDABABCABC" 中查找 pattern = "ABABCAB"。
暴力匹配(Brute Force)的缺点:
- 每次匹配失败后,主串指针回退,模式串从头开始。
- 时间复杂度最坏为 O(n * m),其中 n 是主串长度,m 是模式串长度。
KMP 算法的核心思想是:当匹配失败时,利用已匹配的信息,避免主串指针回退,从而将时间复杂度优化到 O(n + m)。
2. KMP 核心思想:最长相等前后缀(LPS 数组)
KMP 的关键在于构建一个辅助数组:LPS(Longest Proper Prefix which is also Suffix)。
什么是 LPS 数组?
对于模式串 pattern[0..m-1],lps[i] 表示子串 pattern[0..i] 的最长相等真前缀和真后缀的长度。
- 真前缀(Proper Prefix):不等于整个字符串的前缀。
- 真后缀(Proper Suffix):不等于整个字符串的后缀。
举例说明
模式串:"ABABCAB"
| i | pattern[0…i] | 真前缀 | 真后缀 | 最长相等前后缀 | lps[i] |
|---|---|---|---|---|---|
| 0 | A | - | - | (空) | 0 |
| 1 | AB | A | B | 无 | 0 |
| 2 | ABA | A, AB | A, BA | A | 1 |
| 3 | ABAB | A,AB,ABA | B,AB,BAB | AB | 2 |
| 4 | ABABC | … | … | 无 | 0 |
| 5 | ABABCA | … | … | A | 1 |
| 6 | ABABCAB | … | … | AB | 2 |
所以 LPS 数组为:[0, 0, 1, 2, 0, 1, 2]
3. LPS 数组的构造(预处理)
我们使用双指针技巧来构造 LPS 数组:
vector<int> buildLPS(const string& pattern) {
int m = pattern.length();
vector<int> lps(m, 0);
int len = 0; // 当前最长相等前后缀的长度
int i = 1;
while (i < m) {
if (pattern[i] == pattern[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
len = lps[len - 1]; // 回退到更短的相等前后缀
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
解释:
len表示当前pattern[0..i-1]的最长相等前后缀长度。- 如果
pattern[i] == pattern[len],说明可以扩展,len++。 - 如果不相等,且
len > 0,则len = lps[len-1],这是关键!它利用了已计算的信息,避免重新比较。
4. KMP 匹配过程
有了 LPS 数组后,我们就可以进行匹配:
int kmpSearch(const string& text, const string& pattern) {
int n = text.length();
int m = pattern.length();
if (m == 0) return 0;
vector<int> lps = buildLPS(pattern);
int i = 0; // text 的索引
int j = 0; // pattern 的索引
while (i < n) {
if (text[i] == pattern[j]) {
i++;
j++;
}
if (j == m) {
return i - j; // 找到匹配,返回起始位置
} else if (i < n && text[i] != pattern[j]) {
if (j != 0) {
j = lps[j - 1]; // 利用 LPS 跳过不必要的比较
} else {
i++; // j 已经是 0,无法再跳,只能移动 i
}
}
}
return -1; // 未找到匹配
}
关键点:
- 当
text[i] != pattern[j]且j > 0时,j = lps[j-1],表示模式串向右滑动,但i不回退。 - 当
j == 0时,说明模式串已经无法再利用前缀信息,只能移动主串指针i。
5. 完整 C++ 实现
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 构建 LPS 数组
vector<int> buildLPS(const string& pattern) {
int m = pattern.length();
vector<int> lps(m, 0);
int len = 0;
int i = 1;
while (i < m) {
if (pattern[i] == pattern[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
// KMP 字符串匹配
int kmpSearch(const string& text, const string& pattern) {
int n = text.length();
int m = pattern.length();
if (m == 0) return 0; // 空模式串
vector<int> lps = buildLPS(pattern);
int i = 0; // text 索引
int j = 0; // pattern 索引
while (i < n) {
if (text[i] == pattern[j]) {
i++;
j++;
}
if (j == m) {
return i - j; // 找到匹配
} else if (i < n && text[i] != pattern[j]) {
if (j != 0) {
j = lps[j - 1];
} else {
i++;
}
}
}
return -1; // 未找到
}
// 测试函数
int main() {
string text = "ABABDABACDABABCABC";
string pattern = "ABABCAB";
int pos = kmpSearch(text, pattern);
if (pos != -1) {
cout << "模式串在位置 " << pos << " 处找到。" << endl;
} else {
cout << "未找到模式串。" << endl;
}
return 0;
}
6. 时间复杂度分析
- 预处理(构建 LPS):O(m)
- 匹配过程:O(n)
- 总时间复杂度:O(n + m)
空间复杂度:O(m)(用于存储 LPS 数组)
7. 总结
- KMP 算法通过预处理模式串,构建 LPS 数组,避免了主串指针的回退。
- 核心是理解
lps[j-1]的含义:它告诉我们当pattern[j]匹配失败时,模式串应该从哪个位置继续匹配。 - KMP 是字符串匹配的经典算法,是理解自动机、AC 自动机等更高级算法的基础。
1074

被折叠的 条评论
为什么被折叠?



