【考研408数据结构-05】 串与KMP算法:模式匹配的艺术

📚 【考研408数据结构-05】 串与KMP算法:模式匹配的艺术

🎯 考频:⭐⭐⭐⭐⭐ | 题型:选择题、算法设计题 | 分值:约6-13分

引言

想象你在Word文档中使用"查找"功能搜索某个关键词,瞬间就能定位到所有匹配位置。这背后的核心技术就是字符串模式匹配算法 。在408考试中,串的模式匹配特别是KMP算法是几乎每年必考的重点,不仅在选择题中频繁出现,更是算法设计题的高频考点。据统计,近5年的408真题中,有4次直接考察了KMP算法的next数组求解和算法应用。

本文将帮你彻底掌握串的存储结构、BF算法和KMP算法,重点攻克next数组这个难点,让你在考场上游刃有余。

学完本文,你将能够:

  1. ✅ 准确理解串的存储结构和基本操作
  2. ✅ 熟练手工求解next和nextval数组
  3. ✅ 编写完整的KMP算法代码
  4. ✅ 快速解答相关真题

一、知识精讲

1.1 概念定义

串(String) 是由零个或多个字符组成的有限序列,是一种特殊的线性表。

  • 主串(Text):被搜索的字符串,通常用T表示
  • 模式串(Pattern):要查找的字符串,通常用P表示
  • 模式匹配:在主串中查找与模式串相同的子串的过程

💡 408考纲要求

  • 了解:串的基本概念
  • 理解:串的存储结构
  • 掌握:BF算法原理
  • 应用:KMP算法及next数组求解

⚠️ 易混淆点

  • 串的长度:字符的个数(不包括结束符'\0')
  • 串的存储:顺序存储时下标从0还是从1开始(408常用从1开始)

1.2 串的存储结构

串主要有三种存储方式:

存储方式 特点 适用场景
定长顺序存储 用固定长度数组存储 串长度变化不大
堆分配存储 动态分配存储空间 串长度变化较大
块链存储 链表形式,每个节点存多个字符 频繁插入删除
c 复制代码
// 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同时发现。

核心创新

  1. 主串指针永不回溯,始终向前移动
  2. 利用已匹配的信息,通过next数组实现模式串的智能滑动
  3. 时间复杂度降至O(n+m)

关键概念 - 前缀与后缀

  • 前缀:串的前k个字符组成的子串
  • 后缀:串的后k个字符组成的子串
  • 最长公共前后缀:一个串的前缀和后缀的最长相同部分

1.5 next数组详解 ⭐⭐⭐⭐⭐

next数组是KMP算法的灵魂,它记录了模式串中每个位置匹配失败后应该跳转到的位置。

next[j]的含义

当模式串的第j个字符匹配失败时,应该用模式串的第next[j]个字符继续与主串比较。

手工求解next数组的方法

  1. next[1] = 0(固定值)
  2. next[2] = 1(固定值)
  3. 对于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]

二、代码实现

c 复制代码
#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 a

Step 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=1

Step 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数组。

解题思路

  1. 按照next数组定义,逐位计算
  2. 注意下标从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]开始继续匹配。

⚠️ 易错点

  1. 注意下标是从0还是从1开始
  2. next数组和nextval数组的区别
  3. 手工计算时容易遗漏某些前后缀

五、在线练习推荐

LeetCode相关题目

练习顺序建议

  1. 先练习手工求解next数组(大量练习)
  2. 实现基础BF算法
  3. 实现KMP算法
  4. 尝试nextval数组优化
  5. 解决实际应用题

六、思维导图

复制代码
中心主题:串与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序列比对
  • 网络安全:入侵检测系统的模式匹配
  • 搜索引擎:关键词快速定位

常见误区

  1. ❌ next数组的值等于最长公共前后缀长度
    ✅ next数组的值等于最长公共前后缀长度+1
  2. ❌ KMP算法在所有情况下都比BF快
    ✅ 对于短串或随机串,BF可能更快(常数小)
  3. ❌ nextval数组总是比next数组好
    ✅ nextval增加了预处理时间,要权衡使用

记忆技巧

"看前缀,找后缀,最长公共加个1" - 记住next数组求解口诀

结语

串的模式匹配是408数据结构中的必考重点,KMP算法更是其中的核心。通过本文的学习,相信你已经掌握了:

  1. 🎯 串的基本概念和存储结构
  2. 🎯 BF算法的原理和局限性
  3. 🎯 KMP算法的核心思想
  4. 🎯 next数组的手工求解方法
  5. 🎯 完整的代码实现

KMP算法的思想------利用已有信息避免重复工作,这不仅是算法设计的精髓,也将贯穿整个数据结构的学习。下一篇文章,我们将进入树的世界,探索《树与二叉树(上):遍历算法全解析》,掌握另一个408必考重点。

💪 学习建议:KMP算法理解需要时间,建议多画图、多手工模拟。记住,考场上手工求next数组的准确率比代码实现更重要!加油,考研人!


备注:本文所有代码均符合C99标准,适用于408统考要求。

相关推荐
CoovallyAIHub17 分钟前
标注成本骤降,DINOv3炸裂发布!冻结 backbone 即拿即用,性能对标SOTA
深度学习·算法·计算机视觉
BB学长18 分钟前
流固耦合|01流固耦合分类
人工智能·算法
汤永红31 分钟前
week3-[分支嵌套]方阵
c++·算法·信睡奥赛
广州智造35 分钟前
EPLAN教程:流体工程
开发语言·人工智能·python·算法·软件工程·软件构建
闪电麦坤951 小时前
数据结构:从前序遍历序列重建一棵二叉搜索树 (Generating from Preorder)
数据结构··二叉搜索树
闪电麦坤951 小时前
数据结构:二叉树的遍历 (Binary Tree Traversals)
数据结构·二叉树·
球king1 小时前
数据结构中邻接矩阵中的无向图和有向图
数据结构
自信的小螺丝钉1 小时前
Leetcode 343. 整数拆分 动态规划
算法·leetcode·动态规划
Q741_1471 小时前
C++ 力扣 438.找到字符串中所有字母异位词 题解 优选算法 滑动窗口 每日一题
c++·算法·leetcode·双指针·滑动窗口
Fine姐1 小时前
数据挖掘3.6~3.10 支持向量机—— 核化SVM
算法·支持向量机·数据挖掘