Appearance
KMP 算法
2026 大纲 六(八)字符串模式匹配 的主篇(先读《BF 算法》,本篇直接接在它的结论上)。
KMP 只改了 BF 的一行
BF 的失败方式很具体:失配的一瞬间,j = 1,本趟辛辛苦苦比对上的 i 也退回本趟起点的下一位重来。
但那
落到代码上,KMP 与 BF 只差两处:
c
// BF: else { i = i - j + 2; j = 1; }
// KMP: else { j = next[j]; }i 从 else 分支里彻底消失了——这就是"主串指针不回溯"的全部实现。剩下的所有内容,都是在回答两个问题:next 表里该填什么,以及它怎么算出来。
滑动距离为什么只由模式串决定
这是 KMP 唯一需要真正想明白的地方,想通了 next 的定义就是顺出来的。
设本趟比到
现在把问题精确地提出来:
而由已知的部分匹配结果,
式 (2) 里已经没有主串了。 它只对模式串提要求:
这就是全部魔法的来源。滑动距离只由模式串自身决定,可以在还没看到主串的时候一次性算好;而为了不漏解又滑得尽量远,
那个 +1 从哪来。 令
而"长度为
最后是哨兵。 i++, j++,相当于模式串整体右移一位、从头再比。这一步不算一次字符比较——数比较次数的题会用到。另外由定义可直接得出恒等式
先看一眼
下面的可视化把 next 表和匹配过程并排画了出来。看的时候盯住两件事:失配时
看完你应该确认:
两种 next 口径怎么认(做题前必须先分清)
同一个模式串,不同教材给出的 next 表可以整体差 1,甚至下标范围都不同。 这不是谁对谁错,是两套编号约定。而 408 真题两套都出现过,所以做题第一步是先认口径。
| 位置口径(本文,1 起点) | 长度口径(0 起点) | |
|---|---|---|
| 下标范围与哨兵 | ||
| 存的是什么 | 失配后该比第几个字符(位置) | 最长相等前后缀的长度(0 起点下也正是下一个下标) |
| 失配时怎么写 | j = next[j];j == 0 时 i++, j++ | j = next[j];j == -1 时 i++, j++ |
| 0 1 1 2 2 3 1 2 | −1 0 0 1 1 2 0 1 | |
| 换算 | — |
怎么判,按可靠性排序:
① 题面给了定义式或填好的部分表格,一律照题面。 这是最可靠的依据——真题会在题干里写清楚"约定 next[j] = ……"。
② 看首两项。 位置口径是 0、1;长度口径首项是 "abaabcac" 对应 0 0 1 1 2 0 1 0,换算关系是
③ 看代码里的哨兵。 j == 0 是位置口径,j == -1 是长度口径。
本文与站内可视化统一采用位置口径(与严蔚敏教材一致)。认错口径会让整张表错位一格,后面全错。
手工求 next:一个 一行,三处容易错
手工求法就一句口诀:看第
具体到每个
以
| 最长相等前后缀 | |||||
|---|---|---|---|---|---|
| 1 | a | (空) | — | — | 0(规定) |
| 2 | b | "a" | 无 | 0 | 1 |
| 3 | a | "ab" | 无 | 0 | 1 |
| 4 | a | "aba" | "a" | 1 | 2 |
| 5 | b | "abaa" | "a" | 1 | 2 |
| 6 | c | "abaab" | "ab" | 2 | 3 |
| 7 | a | "abaabc" | 无 | 0 | 1 |
| 8 | c | "abaabca" | "a" | 1 | 2 |
三处容易做错的地方,都在这张表里:
- 第 5 行:
"abaa"的相等前后缀是"a"而不是"aa"——后缀确实是"aa",但长度 2 的前缀是,不是 "aa"。前缀必须从第 1 个字符起连续取,不能挑着取。 - 第 6 行:
"abaab"要取"ab"()而不是 "a"()——要最长的。 - 第 7 行:
"abaabc"以c结尾、以a开头,连长度 1 的相等前后缀都没有,, 。
next 的形式化定义式(想看严格表述时展开)
三行分别对应三种处境:
代码:两段,结构一模一样
c
// 求 next 数组(串下标从 1 开始,T[0] 闲置不用)
void getNext(char T[], int next[], int m) {
int j = 1, k = 0; // j 走的是 T 自己(当主串),k 是模式串指针
next[1] = 0; // 哨兵
while (j < m) { // 只算到 next[m],写成 j <= m 会越界
if (k == 0 || T[j] == T[k]) {
j++; k++;
next[j] = k; // 赋的是新 j、新 k:长度 k-1 → 位置 k,即"ℓ+1"
} else {
k = next[k]; // 扩展不下去,退到次长的相等前后缀继续试
}
}
}
// KMP 匹配;返回首次出现的起始位置,失败返回 0
int KMP(char S[], char T[], int next[], int n, int m) {
int i = 1, j = 1;
while (i <= n && j <= m) {
if (j == 0 || S[i] == T[j]) {
i++; j++; // 主串指针只会前进,从不回退
} else {
j = next[j]; // 失配:i 不动,模式串右滑到 next[j]
}
}
return (j > m) ? i - m : 0; // i 停在匹配段之后一位
}j == 0 必须写在 || 的左边:短路求值保证这时不去访问闲置的 T[0]。
把两段并排看,会发现求 next 就是"模式串对自身做一次 KMP"——k = next[k] 回退):
| KMP 匹配 | 求 next | |
|---|---|---|
| 谁在往前走(永不后退) | 主串指针 | 模式串指针 |
| 谁在回退 | 模式串指针 | 辅助指针 |
| 比较的两个字符 | ||
| 相等时 | ||
| 不等时 | ||
| 退到 0 时 |
代码里之所以不能对每个 k = next[k] 这条递推:
递推的正确性证明与 getNext 逐步执行(想彻底弄懂求 next 的代码就展开)
已知
情形 A:
相等前后缀的长度从
情形 B:
关键结论:
证明只要一行:设长度
于是次长相等前后缀的长度
getNext 在 — 表示该次迭代没有写入 next):
| 迭代 | 进入时 | 判断 | 动作 | 写入 |
|---|---|---|---|---|
| 1 | 1, 0 | |||
| 2 | 2, 1 | — | ||
| 3 | 2, 0 | |||
| 4 | 3, 1 | |||
| 5 | 4, 2 | — | ||
| 6 | 4, 1 | |||
| 7 | 5, 2 | |||
| 8 | 6, 3 | — | ||
| 9 | 6, 1 | — | ||
| 10 | 6, 0 | |||
| 11 | 7, 1 | |||
| — | 8, 2 | 退出 | — |
结果
走一遍匹配:数清楚哪些算比较
以 "a"、"ab"、"abc"、"abca",最长相等前后缀长度为 —、0、0、0、1,故
匹配过程:
| 步 | 动作 | |||
|---|---|---|---|---|
| 1 | 1 | 1 | ||
| 2 | 2 | 2 | ||
| 3 | 3 | 3 | ||
| 4 | 3 | 1 | ||
| 5 | 4 | 2 | ||
| 6 | 5 | 3 | ||
| 7 | 6 | 4 | ||
| 8 | 7 | 5 | ||
| 9 | 7 | 2 | ||
| 10 | 8 | 3 | ||
| 11 | 9 | 4 | ||
| 12 | 10 | 5 |
这张表的每一行都算一次字符比较,共 12 次,同一组输入 BF 需要 16 次。数比较次数的题就照这个格式列表,两条规矩要认准:
- 失配的那一次也算一次比较(第 3 步、第 8 步)——"
与 "是真真切切比过的,不能因为结果是"不等"就漏掉; 是查表跳转,不算比较——回退本身不产生字符比较,回退之后在新位置上的那一次才算(第 4 步、第 9 步)。
比 BF 少的那 4 次不是重点。真正的差别是两处结构性事实:
为什么是 :只讲" 不回退"是半个证明
"主串指针不回溯"只说明 next 回退很多次。要真的证出
每次循环迭代恰好落进两个分支之一:
| 分支 | 对 | 对 |
|---|---|---|
| 前进分支( | ||
| 回退分支(失配) | 不变 |
四步就数完:
- 前进分支次数
: 从 1 出发、单调递增,循环条件要求 。 的增量总和 前进分支次数 : 只在前进分支加 1。 的减量总和 增量总和 的初值 : 始终非负,减掉的总量不可能超过"曾经加进去的总量加上起始值"。 - 回退分支次数
的减量总和 :每次回退至少减 1。
两类相加,总迭代次数
预处理阶段的结构与匹配完全同构,把上面四条里的
两条常被记反的边界:①
是最坏情形的上界,不只是平均——这正是相对 BF(最坏 )的核心改进。② 但"最坏更优"不等于"平均更快":一般文本上 BF 的实际执行时间也接近 ,KMP 只在主串与模式串之间存在大量部分匹配时才明显占优。它更本质的价值是主串只需从头到尾扫一遍,可以边读入边匹配。
nextval:把注定要失败的那次比较也跳掉
next 还留了一处浪费。如果
看
修正规则:
并规定
相等时取的是
同一个
| 是否相等 | |||||
|---|---|---|---|---|---|
| 1 | a | 0 | — | — | 0(规定) |
| 2 | b | 1 | a | 1 | |
| 3 | a | 1 | a | 0(取 | |
| 4 | a | 2 | b | 2 | |
| 5 | b | 2 | b | 1(取 | |
| 6 | c | 3 | a | 3 | |
| 7 | a | 1 | a | 0(取 | |
| 8 | c | 2 | b | 2 |
收益有多大,取决于模式串长什么样:"aaaab" 这种长串重复字符差别最大("abcde" 这种各字符互不相同的,
getNextval 的代码(要写代码题时展开)
c
// 求 nextval 数组(串下标从 1 开始,T[0] 闲置不用)
void getNextval(char T[], int nextval[], int m) {
int j = 1, k = 0;
nextval[1] = 0;
while (j < m) {
if (k == 0 || T[j] == T[k]) {
j++;
k++;
if (T[j] != T[k])
nextval[j] = k; // 与 next 相同:退到 k 后还有得比
else
nextval[j] = nextval[k]; // 退到 k 后必然再失配,直接沿用 k 的结果
} else {
k = nextval[k]; // 回退时也用修正后的表,收敛更快
}
}
}与 getNext 的差别只有两处:写入前多一次 T[j] != T[k] 判断;回退用 nextval[k]。注意 T[j] 与 T[k] 都是 j++, k++ 之后的新值,正好对应修正规则里的
考点速记
三条会被反复调用的结论:
- 滑动距离只由模式串自身决定——这是 KMP 能做预处理的唯一理由;+1 只是 1 起点下"长度 → 位置"的换算,换成 0 起点就没有了。
的一半来自" 不回退",另一半来自" 的回退总量不超过 的前进总量",只讲前一半是不完整的证明。 - nextval 只改常数因子,不改量级、也不改匹配结果,且丢掉了"最长相等前后缀"的语义——问语义必须回到 next。
这一节在真题里被考过的形式(下方「真题练习」逐题对应)。整个「串」章的 408 真题全部落在这一篇上,而且三道题正好考了三件不同的事:
- 给失配位置,问下一轮的
和 (2015 年)。题面直接给出"第一次失配时 ",问下次从哪儿开始比。做法固定两步:先算出失配位置的 next 值,再套" 不动、 "。四个选项就是把这两个动作各错一半—— i=1,j=0是 BF 的退法,i=5,j=0是记住了不动却把 归零, i=6,j=2是 next 算对了但跟着 +1。 - 数从头到匹配成功的字符比较次数(2019 年)。列出上面那张逐步表,失配那一次要计入,
回退本身不计入。选项里差 1 的那个就是漏算了失配那次,差 2 的那个是把回退也算成了比较。 - 给"修正后的 next",问模式串向右滑动的最长距离(2024 年)。滑动距离
,对所有 取最大值。这道题的干扰项正是"用原始 next 算"——两张表算出的最大滑动距离不一样,题面写了"修正后"就必须用 nextval。
还有一件事必须先做:认口径。 这三道真题两套编号都出现过——2015 与 2019 那两道在题干里约定的是"
易错:失配时让
也动了。 KMP 里只有匹配成功才让 前进,失配时 一步不退也一步不进。
易错:把
记成 或 。 那等于丢掉已匹配的前缀信息,退化成 BF。
易错:数比较次数时漏掉失配的那一次,或把
的回退也算成比较。 前者少算,后者多算。
易错:题面说"修正后的 next"却用原始 next 去算。 两张表的滑动距离不同,结果直接错。
易错:修正规则写成
。 相等时应取 ,否则跳不到底。
易错:照搬记熟的那套口径去做题。 先读题干的约定,0 起点与 1 起点整体差 1。
教材出处
- 严蔚敏、李冬梅、吴伟民《数据结构(C 语言版)》(第 2 版),第 4 章 4.3.3 节,印刷第 93–94 页:KMP 算法的引入。指出"每当一趟匹配过程中出现字符比较不等时,不需回溯
指针,而是利用已经得到的'部分匹配'的结果将模式向右'滑动'尽可能远的一段距离后,继续进行比较",并给出式 (4-1)~(4-3) 的推导,即本篇式 (1)(2) 的来源。 - 同书印刷第 95 页:next 函数的定义式 (4-4)(三行分段:
取 0、取满足前后缀相等条件的最大 、其他取 1),以及 KMP 匹配过程的文字描述与算法 4.2 Index_KMP。 - 同书印刷第 97 页:算法 4.3「计算 next 函数值」
get_next,并给出的 next 值 (图 4.8);同页指出算法 4.3 的时间复杂度为 。 - 同书印刷第 97 页:next 函数的缺陷说明,以模式
"aaaab"与主串"aaabaaaab"为例,指出、 失配后按 next 还要进行 三次多余比较;由此给出算法 4.4「计算 next 函数修正值」 get_nextval(印刷第 97–98 页),以及"aaaab"的 next与 nextval (图 4.9)。 - 同书印刷第 97 页:「虽然 BF 算法的时间复杂度是
,但在一般情况下,其实际的执行时间近似于 ……KMP 算法仅当模式与主串之间存在许多'部分匹配'的情况下,才显得比 BF 算法快得多。但是 KMP 算法的最大特点是指示主串的指针不需回溯,整个匹配过程中,对主串仅需从头至尾扫描一遍。」
相关知识
BF 算法(对照组与动机来源)| 串的基本概念(下标从 1 的约定,是 +1 与哨兵 0 的前提)| 从 KMP 到工业级全文搜索(超纲延伸)| 查找的基本概念、查找算法的分析及应用