KMP算法
经典问题
- 主串:adbadbadbf
- 模式串:adbadf
问题:在主串中查找一个能够匹配模式串的子串。
朴素解法(BF算法)
解决字符串匹配问题,最先想到的是用暴力匹配法BF。
具体做法:在遍历过程中,当遇到字符不匹配时,主串指针回溯到上一次起始位置的后一位,模式串指针归零,从头开始匹配。
时间复杂度:O(mn)(m 为主串长度,n 为模式串长度)。
改进思路
每次匹配失败,其实前面已经成功比对了一段字符。暴力算法没有利用这部分信息,导致了重复比较。而我们可以利用这部分的工作来减少匹配次数,而模式串在主串上的移动可以看作是从前缀移动到后缀,因此可以从这一点出发。
BF的例子
下面用一个简化的例子来说明 BF 的不足。
第i轮:

第i+1轮:

第j轮:
### 分析
分析第j轮我们发现,模式串中的A和B相等。

由于主串和匹配串匹配时模式串中的 A 与主串中的 C 也相等。也就是说,已经比对过的部分不需要再重新匹配,我们可以直接让模式串的前缀与主串的后缀对齐。
简而言之,当匹配失败时,不必像 BF 那样整体右移一位,而是利用前缀信息,把模式串移动到合适的位置,使前缀和后缀对齐。
基本概念
-
前缀:必包含首字但不包含尾字。如题包含a、ad、adb、adba、adbad。
-
后缀:不包含首字但必包含尾字。如题包含f、df、adf、badf、dbadf。
-
最长相等前后缀:某个子串中,前缀和后缀相等的最大长度字符串。
next 数组(前缀表)
定义与作用
-
定义(长度版):next[j]是以下标 j 结尾子串的最长相等前后缀的长度/前缀后续的第 一位字符的位置。
-
作用:当主串和模式串中某一字符不匹配时,告诉模式串应该回退到哪个位置继续匹配。求前缀表的过程实质是一个动态规划的过程。
示例推导
根据题目中的模式串为例:
| 子串 | a | ad | adb | adba | adbad |
|---|---|---|---|---|---|
| 最长相等前后缀长度 | 0 | 1 | 0 | 1 | 2 |
即next = [0, 1, 0, 1, 2]。
快速计算next的代码
void getNext(int* next, const string& s){
int j=0; // 前缀末尾字符/当前最长相等前后缀的长度 - 前缀0~j
next[0]=0; // 若0处不匹配则回退到0 || 同时这步也意味着跳过"a"
for(int i=1;i<s.size();++i){ // 待匹配的后缀末尾字符 || 此处1是因为只有一个字符时不存在前后缀 - 后缀1~i
while(j>0&&s[i]!=s[j]){ // 前后缀不相等
j=next[j-1];
}
if(s[i]==s[j]) ++j;// 前后缀相等
next[i]=j; // 更新当前字符的next
}
}
流程:初始化→ 遇到不等 → 回退 j → 匹配成功 → 更新 next。
如何回退(核心逻辑)
在求解 next 数组时,最关键的一步就是 回退 j:
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
- 含义:如果当前字符不匹配,就不断缩短“前缀”的长度,尝试更短的前缀是否能继续匹配。
- 为什么是 while? 因为可能需要多次回退,直到找到可以匹配的位置,或者回退到
0为止。
图示回退
-
不需要回退(直接匹配成功)
此时
s[i]==s[j],说明当前字符能直接延长已有前缀,此时只需j++。

👉 **这是最理想情况:**next[i] = next[i-1] + 1。 -
回退后立即成功
当
s[i] != s[j]时,进入while循环:初始比较:
s[i] != s[j],触发一次回退:j = next[j-1]。

回退后重新比较,发现s[i] == s[j],此时匹配成功,更新next[i]。

👉 这是回退一次后匹配成功的情况:next[i] = next[next[i-1]] + 1。 -
回退两次
当
s[i] != s[j]时,进入while循环:第一次比较:
s[i] != s[j],触发一次回退:j = next[j-1]。

第一次比较:
s[i] != s[j],再次触发一次回退:j = next[j-1]。
回退后重新比较,发现 s[i] == s[j],此时匹配成功,更新next[i]。

👉 这是 多次回退 的情况:next[i] = next[next[next[i-1]]] + 1。- 如果最终找到相等字符,则匹配成功。
- 如果退到
0仍不相等,则next[i] = 0。
为什么连续多次回退是对的呢?
设模式串为 P,文本串为 S,当前比较位置 i 在 S,j 在 P。假设已有匹配关系:
P[0..j-1] == S[i-j..i-1]
现在比较 S[i] 与 P[j]:
- 如果相等,则继续扩展;
- 如果不等,根据
next[j-1]的定义,存在长度k = next[j-1]的前缀满足
P[0..k-1] == P[j-k..j-1]
于是可以令 j = k,相当于将模式串前缀 P[0..k-1] 与文本串后缀对齐,再与 S[i] 进行比较。
如果仍不匹配,就继续递归地取 next[k-1]。因为 next 记录了“最长、次长、再次长…”的相等前后缀关系,所以多次回退依次检查所有可能的候选位置,直到找到合适的匹配,或回退到 0 为止。
因此,连续多次回退不会漏掉任何可能的匹配点,也不会做无效比较。
KMP使用next数组来匹配
核心思想
这个过程和求 next 数组时的思路是类似的,都是在失败时不断回退 j,直到找到合适的前后缀对齐位置。
匹配过程中维护两个下标:
i指向主串s;j指向模式串t。
流程:
- 如果
s[i] == t[j],则说明当前字符匹配成功,同时移动i和j; - 如果
s[i] != t[j],则通过next数组回退j,让模式串前缀与已匹配的后缀对齐,从而避免重复比较; - 当
j移动到模式串末尾,说明找到了完整匹配,此时返回匹配的起始位置。
👉 由于每个字符最多被比较两次(一次失败、一次回退后成功),总时间复杂度为 O(m+n)。
具体KMP匹配代码
int KMP(const string &s, const string &t) {
int next[t.size()];
getNext(t, next); // 先构造模式串的 next 数组
int j = 0; // j 表示当前在模式串中的匹配位置
for (int i = 0; i < s.size(); i++) {
// 如果当前字符不匹配,则回退 j
while (j > 0 && s[i] != t[j]) {
j = next[j - 1];
}
// 如果匹配成功,j 向前推进
if (s[i] == t[j]) j++;
// 如果 j 到达模式串末尾,说明完全匹配
if (j == (int)t.size()) {
return i - t.size() + 1; // 返回匹配起始位置
}
}
return -1; // 未找到
}

24万+

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



