← 目录 / 算法文档 · 模块十七 字符串算法 / 17.2 KMP 字符串匹配

17.2 KMP 字符串匹配

在一段长文本里找一个模式串出现的所有位置——失配时不用从头重新比较,靠模式串自己的"前后缀信息"跳过注定失败的尝试。

本页目录
① 为什么需要 KMP:朴素匹配的重复劳动

在文本串 s(长度 n)里查找模式串 p(长度 m)出现的位置,最直接的办法是:从文本的每一个位置开始,逐字符和模式串比较,一旦某个位置失配,就挪到下一个起始位置重新从头比较。这种朴素做法最坏情况下要比较 O(n×m) 次——如果文本和模式串都很长,会非常慢。

朴素做法慢在哪?失配之后把已经比较过的信息全部扔掉,重新从模式串的第一个字符开始比较——但其实已经匹配的这一段字符里,往往藏着有用的信息:"模式串自己的某一段前缀,恰好和已经匹配上的这段文本的某个后缀相同",可以利用这一点,直接跳到一个更靠后、不会白费功夫的位置继续比较。KMP 算法(Knuth-Morris-Pratt)就是把这份信息预先算好,失配时查表就知道该跳到哪。

② 核心思想:next 数组记录"最长相同前后缀"

KMP 的核心是给模式串 p 预处理一个 next 数组(也叫失配函数):next[i] 表示 p[1..i] 这一段前缀里,最长的、同时也是后缀的"真前缀"长度("真"指不能是整个 p[1..i] 自身)。这个信息只和模式串本身有关,和文本串无关,可以提前一次性算好。

③ 图解:构建模式串的 next 数组

以模式串 p = "abab"m=4)为例:

模式串
1
a
2
b
3
a
4
b
next 数组:p[1..i] 的最长相同真前后缀长度
i1234
前缀 p[1..i]aababaabab
next[i]0012
next[3]=1:前缀 "aba" 里,真前缀有 "a""ab",真后缀有 "a""ba",最长的共同部分是 "a"(长度 1)。next[4]=2:前缀 "abab" 里,真前缀 "aba" 和真后缀 "bab" 不同,但真前缀 "ab" 和真后缀 "ab" 相同(长度 2),这是能找到的最长匹配。
④ 图解:利用 next 数组进行匹配

在文本串 s = "ababdabab"n=9)里查找模式串 p = "abab" 出现的所有位置。i 是文本指针,j 是模式串指针,两者都从 1 开始,s[i]p[j] 相等就同时前进;失配时,如果 j>1,就跳到 j = next[j-1] + 1 继续比较(不移动 i);如果 j=1 还失配,只能移动 i

匹配过程逐步追踪
ijs[i]p[j]结果下一步
11aa匹配i=2, j=2
22bb匹配i=3, j=3
33aa匹配i=4, j=4
44bb匹配j 达到 m=4
j 达到模式串长度 4 —— 在位置 i-m+1 = 1 处找到一次匹配!继续查找:j = next[4] = 2(不回退 i)
52db失配j>1,跳到 j = next[1]+1 = 1
51da失配j=1,移动 i:i=6
61aa匹配i=7, j=2
72bb匹配i=8, j=3
83aa匹配i=9, j=4
94bb匹配j 达到 m=4
j 再次达到 4 —— 在位置 i-m+1 = 6 处找到第二次匹配!
全程 i 只从 1 单调增加到 9一次都没有回退——真正回退、重新比较的只有指针 j,而且 j 的回退是靠查 next 表直接跳到位,不需要逐个尝试。最终在文本的第 1 位和第 6 位各找到一次 "abab"
💡
为什么"跳到 next[j-1]+1"是安全的?失配发生在 j 位置,说明 s 里已经匹配上的这一段,恰好等于模式串的前缀 p[1..j-1]next[j-1] 告诉我们:p[1..j-1] 这段前缀本身,又恰好在结尾处重复了长度为 next[j-1] 的一段(既是前缀也是后缀)。也就是说,已经匹配上的文本末尾那一小段,天然就和模式串新的前缀 p[1..next[j-1]] 对得上,不需要再重新验证,直接从 p[next[j-1]+1] 继续比较就行,这正是省下重复比较的关键。
⑤ 完整代码
C++ · KMP 字符串匹配
1int nxt[MAXM]; // nxt[i]:p[1..i] 的最长相同真前后缀长度
2
3void BuildNext(const string& p) // p 下标从 0 开始,p[i-1] 对应第 i 个字符
4{
5 int m = p.size();
6 nxt[1] = 0;
7 int j = 0; // j:当前已经匹配上的前后缀长度
8 for (int i = 2; i <= m; i++)
9 {
10 while (j > 0 && p[i-1] != p[j]) // ★ 失配就用 next 数组回退 j(自己给自己做 KMP)
11 j = nxt[j];
12 if (p[i-1] == p[j]) j++;
13 nxt[i] = j;
14 }
15}
16
17void KmpSearch(const string& s, const string& p)
18{
19 int n = s.size(), m = p.size();
20 BuildNext(p);
21 int j = 0; // j:模式串已经匹配到第几位
22 for (int i = 1; i <= n; i++)
23 {
24 while (j > 0 && s[i-1] != p[j]) // ★ 失配,查 next 表跳转 j,i 不动
25 j = nxt[j];
26 if (s[i-1] == p[j]) j++;
27 if (j == m) // j 达到模式串长度,说明匹配上了一次
28 {
29 cout << "匹配位置:" << i - m + 1 << endl;
30 j = nxt[j]; // 继续查找下一次匹配,不重置为 0
31 }
32 }
33}
💡
构建 next 数组的代码,本质是"用 KMP 匹配自己":第 10~13 行的结构,和第 24~26 行匹配文本串的结构几乎一模一样——因为构建 next 数组,就是在拿模式串的每个前缀去匹配模式串自己。理解了下面 KmpSearch 的匹配逻辑,回头看 BuildNext 会发现它们是同一套代码的两种应用。
⑥ 常见陷阱
next 数组的下标定义不统一,容易记混:不同教材、不同代码对 next 数组的下标定义(是否从 0 开始、next[i] 到底对应"前 i 个字符"还是"前 i-1 个字符")存在多种写法,直接照抄网上代码片段容易和自己使用的约定对不上。写代码前先明确自己用的是哪一种定义,前后保持一致。
找到一次匹配后,把 j 重置为 0 重新开始:第 30 行 j = nxt[j] 而不是直接 j = 0——重置为 0 虽然也能继续找到后续的匹配,但会丢弃"当前已匹配的这一段末尾,可能也是模式串前缀的一部分"这个信息,导致某些重叠的匹配被漏掉(比如模式串本身首尾有重复结构、多次匹配相互重叠的情况)。
把 KMP 的匹配指针 i 也去回退:KMP 最核心的效率保证就是"文本指针 i 全程只增不减",回退的只有模式串指针 j。如果实现中不小心让 i 也发生回退(比如照搬朴素匹配的框架又混入了 next 跳转),会失去 KMP O(n+m) 的复杂度保证,退化成接近朴素匹配的效率。
🏆
接下来:KMP 解决的是"一个模式串在文本里出现的位置",17.3 节的 Trie 字典树会换一个场景——如果需要同时处理很多个字符串的前缀信息(比如判断某个字符串是不是词典里某些单词的前缀),会用一种树形结构来高效组织,而不是逐个字符串分别处理。