Appearance
408
数据结构
字符串模式匹配(BF/KMP)
定位信息
| 项目 | 内容 |
|---|---|
| 章节 | 第六章 查找 · 6.8 |
| 知识单元编号 | DS-06-08 |
| 核心概念 | BF 算法(暴力匹配)、KMP 算法、next 数组、nextval 数组 |
| 前置知识 | 字符串基本操作 |
| 考试权重 | ★★★★★(极高频,综合题必考 KMP 的 next/nextval 数组计算) |
知识点讲解
一、模式匹配的基本概念
模式匹配(Pattern Matching): 在主串 S 中查找模式串 T 的第一次出现位置。S 长度为 n,T 长度为 m。
目标: 返回 T 在 S 中首次出现的起始下标(或 0 表示匹配失败)。
二、BF 算法(Brute-Force,朴素匹配)
基本思想: 从主串 S 的每个位置开始,逐字符与模式串 T 比较。若某位不匹配,S 回退到上次开始位置的下一个,T 回退到开头。
时间复杂度: 最坏 O(n × m),平均 O(n + m)。
BF 算法伪代码(C 风格):
c
// BF 模式匹配:在主串 S[1..n] 中查找模式串 T[1..m]
// 返回匹配成功的起始位置,失败返回 0
int BF(char S[], int n, char T[], int m) {
int i = 1; // 主串指针
int j = 1; // 模式串指针
while (i <= n && j <= m) {
if (S[i] == T[j]) {
i++; // 匹配成功,继续比较下一位
j++;
} else {
i = i - j + 2; // 主串指针回退到上次开始的下一位
j = 1; // 模式串指针回退到开头
}
}
if (j > m)
return i - m; // 匹配成功,返回起始位置
else
return 0; // 匹配失败
}BF 的核心问题: 每次失配时,主串指针 i 回退,导致大量重复比较。KMP 算法通过分析模式串的结构,避免了 i 的回退。
三、KMP 算法(Knuth-Morris-Pratt)
核心思想: 利用已匹配的信息,当失配发生时,主串指针 i 不回退,只将模式串指针 j 移动到一个合适的位置(由 next 数组决定)。
关键概念——前缀、后缀、最长公共前后缀:
- 前缀: 字符串的所有以第一个字符开头的子串(不含自身);
- 后缀: 字符串的所有以最后一个字符结尾的子串(不含自身);
- 最长公共前后缀(LPS): 前缀和后缀中最长的相同子串的长度。
next 数组的定义:
next[j] 的含义:当 T[j] 与 S[i] 失配时,j 应移动到 next[j] 位置继续比较。
next[j] 的值 = T[1..j-1] 的最长公共前后缀长度 + 1
| j | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| T[j] | a | b | a | a | b | c | a |
| next[j] | 0 | 1 | 1 | 2 | 2 | 3 | 1 |
- next[1] = 0(特殊规定,失配时 i 前进,j 回到 0)
- next[2] = 1("a" 的最长公共前后缀长度 = 0,+1 = 1)
- next[3] = 1("ab" 无公共前后缀,长度 = 0,+1 = 1)
- next[4] = 2("aba" 的最长公共前后缀 = "a",长度 = 1,+1 = 2)
- ...
KMP 算法伪代码(C 风格):
c
// KMP 模式匹配
// next[] 数组已预先计算好
int KMP(char S[], int n, char T[], int m, int next[]) {
int i = 1; // 主串指针(不回退!)
int j = 1; // 模式串指针
while (i <= n && j <= m) {
if (j == 0 || S[i] == T[j]) {
i++;
j++; // 匹配成功或 j=0 时,两指针均前进
} else {
j = next[j]; // 失配:i 不动,j 跳转到 next[j]
}
}
if (j > m)
return i - m; // 匹配成功
else
return 0; // 匹配失败
}四、next 数组的求法(手动计算步骤)
对模式串 T[1..m],逐位计算 next[j]:
- next[1] = 0(固定值)
- next[2] = 1(固定值)
- 对于 j ≥ 3:看 T[1..j-1] 的最长公共前后缀长度 len,next[j] = len + 1
手动求法口诀: "看前 j-1 个字符,最长相等前后缀,长度加 1。"
next 数组计算伪代码(C 风格):
c
// 求模式串 T[1..m] 的 next 数组
void GetNext(char T[], int m, int next[]) {
next[1] = 0;
int j = 1, k = 0; // k 记录当前最长公共前后缀长度
while (j < m) {
if (k == 0 || T[j] == T[k]) {
j++;
k++;
next[j] = k; // next[j+1] = next[j] + 1(递推)
} else {
k = next[k]; // k 回退,类似 KMP 匹配过程
}
}
}五、nextval 数组(优化的 next 数组)
next 的缺陷: 当 T[j] 失配且 T[next[j]] == T[j] 时,跳转到 next[j] 仍然会失配,浪费一次比较。
nextval 的改进: 如果 T[next[j]] == T[j],则 nextval[j] = nextval[next[j]],继续跳转,直到找到不同的字符或回到 0。
nextval 计算规则:
- nextval[1] = 0
- 对于 j ≥ 2:如果 T[next[j]] ≠ T[j],则 nextval[j] = next[j];如果 T[next[j]] == T[j],则 nextval[j] = nextval[next[j]]
六、BF vs KMP 对比表
| 对比维度 | BF 算法 | KMP 算法 |
|---|---|---|
| 主串指针 i | 失配时回退 | 不回退 |
| 模式串指针 j | 失配时回到 1 | 失配时跳转到 next[j] |
| 最坏时间复杂度 | O(n × m) | O(n + m) |
| 预处理 | 无 | 需计算 next 数组 O(m) |
| 适用场景 | 短模式串或偶尔匹配 | 长模式串或多次匹配 |
| 空间复杂度 | O(1) | O(m)(next 数组) |
next 与 nextval 对比表:
| 对比维度 | next 数组 | nextval 数组 |
|---|---|---|
| 计算基础 | 最长公共前后缀 | 在 next 基础上优化 |
| 失配效率 | 可能多次无效跳转 | 避免无效跳转 |
| 匹配次数 | 较多 | 较少(更优) |
| 计算复杂度 | O(m) | O(m) |
| 408 考试频率 | 极高 | 高(next 的升级版) |
记忆辅助
- "BF 回退 i 和 j,KMP 只退 j 不退 i" — 最核心区别。KMP 的精髓是主串指针不回退。
- "next 看前缀,最长相等前后缀长度 +1" — 手动求 next 的口诀。
- "nextval 防重复,相等就跳过" — nextval 优化:如果跳转后字符相同,继续跳。
- "KMP 时间 O(n+m),next 数组 O(m)" — 总时间 = 预处理 + 匹配 = O(m) + O(n+m) = O(n+m)。
例题精解
例题 1:手动求 next 数组
求模式串 T = "abaabcac" 的 next 数组。
解题四步法:
① 审题: T 长度 m = 8,逐位求 next[j]。
② 分析: next[1]=0, next[2]=1,后续看 T[1..j-1] 的最长公共前后缀。
③ 过程:
| j | T[j] | T[1..j-1] | 最长公共前后缀 | 长度 | next[j] |
|---|---|---|---|---|---|
| 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 |
next 数组:{0, 1, 1, 2, 2, 3, 1, 2}
④ 结论: 逐位分析最长公共前后缀长度即可得到 next 数组。关键位:j=6 时 "abaab" 的最长公共前后缀为 "ab"(长度 2),next[6] = 3。
例题 2:KMP 匹配过程模拟
主串 S = "ababaabaabcacbabc",模式串 T = "abaabcac",next = {0,1,1,2,2,3,1,2},用 KMP 算法模拟匹配过程。
解题四步法:
① 审题: 用 KMP 算法在 S 中查找 T,展示匹配过程。
② 分析: 按 KMP 规则:匹配则 i++,j++;失配则 i 不动,j = next[j]。
③ 过程(关键步骤):
主串 S: a b a b a a b a a b c a c b a b c
下标: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
第1轮:i=1,j=1 → S[1]=a=T[1] ✓
i=2,j=2 → S[2]=b=T[2] ✓
i=3,j=3 → S[3]=a=T[3] ✓
i=4,j=4 → S[4]=b≠T[4]=a ✗
失配!j=next[4]=2,i 保持 4
第2轮:i=4,j=2 → S[4]=b=T[2] ✓
i=5,j=3 → S[5]=a=T[3] ✓
i=6,j=4 → S[6]=a=T[4] ✓
i=7,j=5 → S[7]=b=T[5] ✓
i=8,j=6 → S[8]=a≠T[6]=c ✗
失配!j=next[6]=3,i 保持 8
第3轮:i=8,j=3 → S[8]=a=T[3] ✓
i=9,j=4 → S[9]=a=T[4] ✓
i=10,j=5 → S[10]=b=T[5] ✓
i=11,j=6 → S[11]=c=T[6] ✓
i=12,j=7 → S[12]=a=T[7] ✓
i=13,j=8 → S[13]=c=T[8] ✓
j=8>8,匹配成功!返回 i-m = 13-8 = 5匹配成功位置:S 从下标 5 开始 = "abaabcac"
④ 结论: KMP 算法通过 next 数组避免了主串指针回退,总共比较约 14 次(远少于 BF 的最坏情况)。关键在于失配时 j 跳转到 next[j],i 保持不变。
考情分析
| 维度 | 说明 |
|---|---|
| 考频 | 极高频,几乎每年必考 |
| 题型 | 选择题(next 数组值判断、KMP 与 BF 对比)、综合题(手动求 next/nextval 数组 + 匹配过程) |
| 命题趋势 | next 数组的手动计算是必考题;nextval 数组的求法是进阶考点 |
| 常见陷阱 | next[1]=0 和 next[2]=1 是固定值容易忘;最长公共前后缀不含字符串自身 |
易错点
- next[1] = 0,next[2] = 1,这是固定规定! 不需要计算,直接写。失配时 j=next[1]=0 意味着 i 前进一位,j 回到开头重新开始。
- 最长公共前后缀不含字符串本身。 例如 "aba" 的前缀是 {"a", "ab"},后缀是 {"a", "ba"},公共部分是 {"a"},长度为 1。
- KMP 的时间复杂度是 O(n + m),不是 O(n × m)。 主串指针 i 不回退是关键,i 最多移动 n 次。
- nextval 的优化方向:如果 T[next[j]] == T[j],则 nextval[j] = nextval[next[j]]。 这样可以跳过重复字符,减少无效比较。
- BF 的主串指针回退公式:i = i - j + 2。 这是回到"上次匹配起点的下一位",容易计算错误。
来源标注
| 来源 | 说明 |
|---|---|
| 王道考研 | 2026 版数据结构讲义 · 第六章 查找 |
| 天勤考研 | 数据结构高分笔记 · 查找部分 |
| 严蔚敏 | 《数据结构(C 语言版)》第 4 章 |
| 教材参考 | 《数据结构》(第 5 版)李春葆 · 串章节 |