【考研408数据结构-05】 串与KMP算法:模式匹配的艺术
📚 【考研408数据结构-05】 串与KMP算法:模式匹配的艺术
🎯 考频:⭐⭐⭐⭐⭐ | 题型:选择题、算法设计题 | 分值:约6-13分
引言
想象你在Word文档中使用"查找"功能搜索某个关键词,瞬间就能定位到所有匹配位置。这背后的核心技术就是字符串模式匹配算法。在408考试中,串的模式匹配特别是KMP算法是几乎每年必考的重点,不仅在选择题中频繁出现,更是算法设计题的高频考点。据统计,近5年的408真题中,有4次直接考察了KMP算法的next数组求解和算法应用。
本文将帮你彻底掌握串的存储结构、BF算法和KMP算法,重点攻克next数组这个难点,让你在考场上游刃有余。
学完本文,你将能够:
- ✅ 准确理解串的存储结构和基本操作
- ✅ 熟练手工求解next和nextval数组
- ✅ 编写完整的KMP算法代码
- ✅ 快速解答相关真题
一、知识精讲
1.1 概念定义
串(String) 是由零个或多个字符组成的有限序列,是一种特殊的线性表。
- 主串(Text):被搜索的字符串,通常用T表示
- 模式串(Pattern):要查找的字符串,通常用P表示
- 模式匹配:在主串中查找与模式串相同的子串的过程
💡 408考纲要求:
- 了解:串的基本概念
- 理解:串的存储结构
- 掌握:BF算法原理
- 应用:KMP算法及next数组求解
⚠️ 易混淆点:
- 串的长度:字符的个数(不包括结束符’\0’)
- 串的存储:顺序存储时下标从0还是从1开始(408常用从1开始)
1.2 串的存储结构
串主要有三种存储方式:
存储方式 | 特点 | 适用场景 |
---|---|---|
定长顺序存储 | 用固定长度数组存储 | 串长度变化不大 |
堆分配存储 | 动态分配存储空间 | 串长度变化较大 |
块链存储 | 链表形式,每个节点存多个字符 | 频繁插入删除 |
// 408考试常用:定长顺序存储(下标从1开始)
#define MAXLEN 255
typedef struct {char ch[MAXLEN + 1]; // ch[0]不用,从ch[1]开始存储int length; // 串的实际长度
} SString;
1.3 BF算法(Brute Force)
暴力匹配算法是最直观的模式匹配方法:
核心思想:从主串的第一个字符开始,依次与模式串比较。若匹配失败,主串指针回溯,从下一个位置重新开始匹配。
时间复杂度:O(n×m),其中n是主串长度,m是模式串长度
缺点:存在大量重复比较,效率低下
1.4 KMP算法原理 🎯
KMP算法(Knuth-Morris-Pratt)是一种高效的模式匹配算法,由D.E.Knuth、J.H.Morris和V.R.Pratt同时发现。
核心创新:
- 主串指针永不回溯,始终向前移动
- 利用已匹配的信息,通过next数组实现模式串的智能滑动
- 时间复杂度降至O(n+m)
关键概念 - 前缀与后缀:
- 前缀:串的前k个字符组成的子串
- 后缀:串的后k个字符组成的子串
- 最长公共前后缀:一个串的前缀和后缀的最长相同部分
1.5 next数组详解 ⭐⭐⭐⭐⭐
next数组是KMP算法的灵魂,它记录了模式串中每个位置匹配失败后应该跳转到的位置。
next[j]的含义:
当模式串的第j个字符匹配失败时,应该用模式串的第next[j]个字符继续与主串比较。
手工求解next数组的方法:
- next[1] = 0(固定值)
- next[2] = 1(固定值)
- 对于j>2:找出P[1…j-1]的最长公共前后缀长度k,则next[j] = k + 1
示例:求模式串"ababcab"的next数组
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
模式串 | a | b | a | b | c | a | b |
next[j] | 0 | 1 | 1 | 2 | 3 | 1 | 2 |
1.6 nextval数组优化
nextval数组是对next数组的优化,避免了无效的比较。
优化原则:
如果P[j] = P[next[j]],则nextval[j] = nextval[next[j]];
否则,nextval[j] = next[j]
二、代码实现
#include <stdio.h>
#include <string.h>#define MAXLEN 255// 串的定义(下标从1开始)
typedef struct {char ch[MAXLEN + 1]; // ch[0]不用int length;
} SString;// BF算法实现
int BF(SString T, SString P) {int i = 1, j = 1; // i指向主串,j指向模式串while (i <= T.length && j <= P.length) {if (T.ch[i] == P.ch[j]) {i++;j++;} else {i = i - j + 2; // 主串指针回溯j = 1; // 模式串从头开始}}if (j > P.length) {return i - P.length; // 匹配成功,返回起始位置}return 0; // 匹配失败
}// 求next数组
void getNext(SString P, int next[]) {int j = 1, k = 0;next[1] = 0;while (j < P.length) {if (k == 0 || P.ch[j] == P.ch[k]) {j++;k++;next[j] = k;} else {k = next[k]; // k回溯}}
}// 求nextval数组(优化版本)
void getNextval(SString P, int nextval[]) {int j = 1, k = 0;nextval[1] = 0;while (j < P.length) {if (k == 0 || P.ch[j] == P.ch[k]) {j++;k++;if (P.ch[j] != P.ch[k]) {nextval[j] = k;} else {nextval[j] = nextval[k]; // 优化}} else {k = nextval[k];}}
}// KMP算法实现
int KMP(SString T, SString P, int next[]) {int i = 1, j = 1; // i指向主串,j指向模式串while (i <= T.length && j <= P.length) {if (j == 0 || T.ch[i] == P.ch[j]) {i++;j++;} else {j = next[j]; // j回溯到next[j]位置}}if (j > P.length) {return i - P.length; // 匹配成功}return 0; // 匹配失败
}// 测试函数
int main() {SString T, P;strcpy(T.ch + 1, "ababcababa");T.length = 10;strcpy(P.ch + 1, "ababa");P.length = 5;int next[MAXLEN];getNext(P, next);int pos = KMP(T, P, next);if (pos) {printf("Pattern found at position: %d\n", pos);} else {printf("Pattern not found\n");}return 0;
}
复杂度分析:
- BF算法:时间O(n×m),空间O(1)
- KMP算法:时间O(n+m),空间O(m)
- 求next数组:时间O(m),空间O(m)
三、图解说明
【图1】BF算法执行流程
主串T: a b a b c a b a b a
模式串P: a b a b aStep 1: i=1, j=1
T: [a] b a b c a b a b a
P: [a] b a b a✓ 匹配Step 2: i=5, j=5
T: a b a b [c] a b a b a
P: a b a b [a]✗ 失配,i回溯到2,j=1Step 3: i=2, j=1
T: a [b] a b c a b a b a
P: [a] b a b a✗ 失配,i=3,j=1
【图2】KMP算法next数组作用
当P[5]='a'与T[5]='c'失配时:
不需要从头开始,而是让j=next[5]=3继续比较T: a b a b c a b a b a
P: a b a b a (失配前)↓
P: a b a b a (使用next[5]=3后)
【图3】前缀后缀示意图
串"ababa"的前缀后缀分析:
位置4: "abab"
前缀:a, ab, aba
后缀:b, ab, bab
最长公共前后缀:"ab",长度=2
所以next[5]=2+1=3
四、真题演练
【2023年408真题】
题目:设主串T=“abaabaabcab”,模式串P=“abaabcab”,采用KMP算法进行匹配,求模式串的next数组。
解题思路:
- 按照next数组定义,逐位计算
- 注意下标从1开始
标准答案:
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
P[j] | a | b | a | a | b | c | a | b |
next[j] | 0 | 1 | 1 | 2 | 2 | 3 | 1 | 2 |
评分要点:
- next[1]=0, next[2]=1(2分)
- 正确计算其他位置(每个1分)
【2021年408真题变式】
题目:已知模式串的next数组为{0,1,1,2,3,1},主串匹配到第10个字符时失配,此时模式串应从第几个字符开始继续匹配?
解答:根据KMP算法,当P[6]失配时,应从P[next[6]]=P[1]开始继续匹配。
⚠️ 易错点:
- 注意下标是从0还是从1开始
- next数组和nextval数组的区别
- 手工计算时容易遗漏某些前后缀
五、在线练习推荐
LeetCode相关题目
- 28. 实现strStr() - 简单
- 459. 重复的子字符串 - 简单
- 214. 最短回文串 - 困难
练习顺序建议
- 先练习手工求解next数组(大量练习)
- 实现基础BF算法
- 实现KMP算法
- 尝试nextval数组优化
- 解决实际应用题
六、思维导图
中心主题:串与KMP算法
├── 一级分支1:基础概念
│ ├── 二级分支1.1:串的定义
│ ├── 二级分支1.2:主串与模式串
│ └── 二级分支1.3:存储结构
├── 一级分支2:BF算法
│ ├── 二级分支2.1:算法思想
│ ├── 二级分支2.2:时间复杂度O(nm)
│ └── 二级分支2.3:指针回溯问题
├── 一级分支3:KMP算法
│ ├── 二级分支3.1:核心思想
│ ├── 二级分支3.2:next数组
│ ├── 二级分支3.3:nextval优化
│ └── 二级分支3.4:时间复杂度O(n+m)
└── 一级分支4:应用场景├── 二级分支4.1:文本搜索├── 二级分支4.2:DNA序列匹配└── 二级分支4.3:网络入侵检测
七、复习清单
✅ 本章必背知识点清单
概念理解
- 能准确说出串的定义和存储方式
- 理解前缀、后缀、最长公共前后缀的概念
- 掌握主串指针回溯与不回溯的区别
代码实现
- 能手写BF算法代码
- 能手写求next数组的代码
- 能手写KMP算法主体代码
- 记住BF时间复杂度为O(n×m)
- 记住KMP时间复杂度为O(n+m)
应用能力
- 会手工求解任意模式串的next数组
- 会手工求解nextval数组
- 能分析不同场景下算法的选择
- 掌握next[1]=0, next[2]=1的固定规律
真题要点
- 理解next数组每个值的具体含义
- 掌握KMP算法的执行过程模拟
- 记住常见陷阱:下标从0还是从1开始
八、知识拓展
前沿应用
KMP算法的思想被广泛应用于:
- 生物信息学:DNA/RNA序列比对
- 网络安全:入侵检测系统的模式匹配
- 搜索引擎:关键词快速定位
常见误区
- ❌ next数组的值等于最长公共前后缀长度
✅ next数组的值等于最长公共前后缀长度+1 - ❌ KMP算法在所有情况下都比BF快
✅ 对于短串或随机串,BF可能更快(常数小) - ❌ nextval数组总是比next数组好
✅ nextval增加了预处理时间,要权衡使用
记忆技巧
“看前缀,找后缀,最长公共加个1” - 记住next数组求解口诀
结语
串的模式匹配是408数据结构中的必考重点,KMP算法更是其中的核心。通过本文的学习,相信你已经掌握了:
- 🎯 串的基本概念和存储结构
- 🎯 BF算法的原理和局限性
- 🎯 KMP算法的核心思想
- 🎯 next数组的手工求解方法
- 🎯 完整的代码实现
KMP算法的思想——利用已有信息避免重复工作,这不仅是算法设计的精髓,也将贯穿整个数据结构的学习。下一篇文章,我们将进入树的世界,探索《树与二叉树(上):遍历算法全解析》,掌握另一个408必考重点。
💪 学习建议:KMP算法理解需要时间,建议多画图、多手工模拟。记住,考场上手工求next数组的准确率比代码实现更重要!加油,考研人!
备注:本文所有代码均符合C99标准,适用于408统考要求。