KMP算法原理详解
KMP算法(Knuth-Morris-Pratt算法)是一种高效的字符串匹配算法,核心思想是通过预处理模式串构建next
数组(部分匹配表),在匹配失败时利用已匹配信息跳过无效比较,将时间复杂度优化至O(n+m)(n为文本串长度,m为模式串长度)。
一、核心原理图解
1. 朴素匹配 vs KMP匹配
朴素匹配在失配时回溯文本串指针,导致重复比较(时间复杂度O(n×m)):
KMP匹配在失配时保持文本串指针不回溯,仅移动模式串:
2. next数组原理
next[j]
表示模式串位置j
之前子串的最长公共前后缀长度:
- 前缀:包含首字符但不包含尾字符的子串
- 后缀:包含尾字符但不包含首字符的子串
示例(模式串"ABABA"):
子串 | 前缀 | 后缀 | 最长公共前后缀 | next值 |
---|---|---|---|---|
A | [] | [] | 无(长度0) | 0 |
AB | [A] | [B] | 无 | 0 |
ABA | [A, AB] | [BA, A] | “A”(长度1) | 1 |
ABAB | [A, AB, ABA] | [BAB, AB, B] | “AB”(长度2) | 2 |
ABABA | [A, AB, ABA, ABAB] | [BABA, ABA, BA, A] | “ABA”(长度3) | 3 |
next数组计算流程(动态规划):
二、匹配过程图解(以文本串"ABABCABAC"和模式串"ABABA"为例)
1. 初始化
文本串:A B A B C A B A C
模式串:A B A B A
next: -1 0 0 1 2
↑j=0
↑i=0
2. 第一轮匹配(i=4时失配)
文本串:A B A B C ...
模式串:A B A B A
↑j=4(失配)
- 已匹配部分:“ABAB”
- 最长公共前后缀:“AB”(长度2)
- 移动位数:4 - next[4] = 4 - 2 = 2
- 新模式串位置:
文本串:A B A B C ...
模式串: A B A B A
↑j=2(next[4]=2)
3. 第二轮匹配(i=4继续比较)
文本串:A B A B C ...
模式串: A B A B A
↑j=2 → 匹配继续
最终匹配成功:i=8, j=5 → 返回8-5=3
三、算法实现解析
1. next数组计算优化
通过动态规划递推求解,避免重复比较:
- 若
P[i] == P[j]
,则next[i+1] = j+1
- 若不等,则
j = next[j]
回溯:
while (i < sub_len - 1) {
if (j == -1 || sub[i] == sub[j]) {
j++; i++;
if (sub[i] == sub[j]) next[i] = next[j]; // 优化:避免重复匹配
else next[i] = j;
} else {
j = next[j]; // 回溯
}
}
2. 匹配过程核心逻辑
四、应用场景
KMP算法在以下领域广泛应用:
- 文本编辑器:快速查找/替换(如Notepad++、VS Code)
- 生物信息学:DNA/RNA序列匹配(如基因片段比对)
- 数据压缩:LZ系列算法中的字典更新优化
- 网络安全:入侵检测系统中的特征码匹配
- 数据库系统:加速文本索引和查询
五、完整源代码
/**
* KMP字符串匹配算法实现
* 功能:在文本串src中查找模式串sub首次出现的位置
* 时间复杂度:O(n + m)
*/
template <typename T>
int FindIndexOf(int* next, T* src, int src_len, T* sub, int sub_len) noexcept {
// 计算next数组的Lambda函数
static constexpr auto FindNextOf =
[](int* next, T* sub, int sub_len) noexcept {
int l = sub_len - 1;
int i = 0;
int j = -1;
next[0] = -1;
while (i < l) {
if (j == -1 || sub[i] == sub[j]) {
j++;
i++;
// 优化:避免重复匹配
if (sub[i] == sub[j]) {
next[i] = next[j];
}
else {
next[i] = j;
}
}
else {
j = next[j];
}
}
};
int i = 0;
int j = 0;
FindNextOf(next, sub, sub_len); // 预处理next数组
// KMP匹配主循环
while (i < src_len && j < sub_len) {
if (j == -1 || src[i] == sub[j]) {
i++;
j++;
}
else {
j = next[j]; // 失配时跳转到next[j]
}
}
// 返回匹配结果
if (j >= sub_len) {
return i - sub_len; // 匹配成功
}
else {
return -1; // 未匹配
}
}
关键改进说明:
next
数组计算中增加sub[i] == sub[j]
判断,避免重复匹配(即nextval
优化)- 模板化设计支持任意数据类型(如
char
、wchar_t
等)- 无异常保证(
noexcept
)适用于高性能场景
六、参考图示
1. next数组计算过程(模式串"ABABCAB"):
步骤解析:
- 初始化:
next[0] = -1
(无前缀),next[1] = 0
(单字符无真前后缀)。 - i=2:
P[2]='A'
匹配P[0]='A'
→next[2]=1
(最长公共前后缀长度=1)。 - i=3:
P[3]='B'
匹配P[1]='B'
→next[3]=2
(公共前后缀"AB"
)。 - i=4:
P[4]='C'
不匹配P[2]='A'
→ 回溯k=next[2]=1
→ 仍不匹配P[1]='B'
→ 回溯至k=0
→ 不匹配P[0]='A'
→next[4]=0
。 - i=5:
P[5]='A'
匹配P[0]='A'
→next[5]=1
。 - i=6:
P[6]='B'
匹配P[1]='B'
→next[6]=2
(公共前后缀"AB"
)。
最终 next 数组:[-1, 0, 1, 2, 0, 1, 2]
。
2. 完整匹配流程图:
流程说明:
3. 初始化:文本指针 i=0
,模式指针 j=0
。
4. 字符匹配:
- 若
S[i] = P[j]
,则i++
,j++
。 - 若
S[i] ≠ P[j]
:- 若
j=0
,则i++
(文本指针后移)。 - 否则
j = next[j]
(模式指针回溯至最长公共前后缀位置)。
- 若
- 匹配成功:当
j = len(P)
时,返回匹配起始位置i - j
。 - 跳转优化:失配时通过
next
数组跳过已匹配前缀,避免文本指针回溯。
3. 关键点总结:
- Next 数组作用:记录模式串的自匹配信息(最长公共前后缀长度),匹配失败时通过
j=next[j]
跳转,使i
不回溯,将时间复杂度优化至 O(n+m)O(n+m)O(n+m) 。 - 匹配流程优势:相比暴力匹配(O(n×m)O(n \times m)O(n×m)),KMP 的文本指针
i
永不回溯,仅模式指针j
根据next
数组调整位置。