数据结构必学:彻底掌握KMP算法(附C++代码与考研模拟题)

 

一、从暴力匹配到KMP:为什么需要它?

 

在日常的文本处理中,我们经常会遇到一个很基础的问题:在一段较长的文本里找到某个特定的短字符串。比如说,我们有一段文本 “ABABABCAA”,想看看其中是否包含 “ABABC” 这个小片段。

 

最容易想到的办法就是一个一个字符去比较,也就是暴力匹配法。从文本的第一个字符开始,和小片段的第一个字符比较,如果一样,就继续比较下一个字符;要是不一样,就把小片段往后挪一个字符,重新从第一个字符开始比较。这个过程中,一旦发现不匹配,文本的指针就得回溯,重新开始新的一轮比较。这样做虽然能实现功能,但效率很低,时间复杂度高达 O(n×m),其中n是文本的长度,m是小片段的长度。

 

KMP算法的出现,很好地解决了这个效率问题。它引入了一个很关键的概念:前缀函数(next数组) 。这个数组记录了小片段自身的匹配信息,有了它,在匹配过程中文本的指针就不用回溯了,大大提升了匹配速度,时间复杂度降低到 O(n+m)。

 

二、KMP的核心:前缀函数与next数组

 

1. 什么是前后缀?

 

- 前缀:就是从小片段开头开始,取一部分字符组成的子串,但不能包含最后一个字符。比如对于 “ABAB” 这个字符串,它的前缀有 “A”、“AB”、“ABA” 。

 

- 后缀:从字符串末尾往前取字符组成的子串,不过不能包含第一个字符。像 “ABAB” 的后缀就有 “B”、“AB”、“BAB” 。

 

- 最长公共前后缀:在一个字符串的所有前缀和后缀里,找长度最长且内容相同的那一对,它们的长度就是最长公共前后缀的长度。比如 “ABAB” 的最长公共前后缀是 “AB”,长度为2 。

 

2. next数组的构建(动态规划思想)

 

next数组里的每个元素  next[i]  ,代表的是小片段前  i  个字符组成的子串的最长公共前后缀长度 。

 

举个例子,对于模式串 “ABABC” ,它的next数组是  [0,0,1,2,0]  。计算过程如下:

 

- 第一个字符,没有前后缀, next[0]=0  。

 

- 前两个字符 “AB”,没有公共前后缀, next[1]=0  。

 

- 前三个字符 “ABA”,最长公共前后缀是 “A”,长度为1, next[2]=1  。

 

- 前四个字符 “ABAB”,最长公共前后缀是 “AB”,长度为2, next[3]=2  。

 

- 前五个字符 “ABABC”,没有公共前后缀, next[4]=0  。

 

下面是用C++ 实现计算next数组的代码:

 

cpp

// 计算next数组

vector<int> computeNext(string pattern) {

    // 模式串的长度

    int m = pattern.size();

    // 初始化next数组,大小为m,初始值都为0

    vector<int> next(m, 0);

    // i从1开始,j初始化为0

    for (int i = 1, j = 0; i < m; i++) {

        // 当j大于0且当前字符和j位置的字符不相等时

        while (j > 0 && pattern[i]!= pattern[j])

            // j更新为next[j - 1]

            j = next[j - 1];

        // 如果当前字符和j位置的字符相等

        if (pattern[i] == pattern[j])

            // j自增1

            j++;

        // 更新next[i]

        next[i] = j;

    }

    // 返回计算好的next数组

    return next;

}

 

 

这段代码的逻辑是,在遍历模式串的过程中,通过不断调整j的值,找到当前位置的最长公共前后缀长度。

 

三、KMP完整代码实现(C++)

 

下面是完整的KMP匹配算法的C++ 代码,它包含了计算next数组和进行字符串匹配的功能:

 

cpp

#include <iostream>

#include <vector>

using namespace std;

 

// 计算next数组

vector<int> computeNext(string p) {

    int m = p.size();

    vector<int> next(m, 0);

    for (int i = 1, j = 0; i < m; i++) {

        while (j > 0 && p[i]!= p[j])

            j = next[j - 1];

        if (p[i] == p[j])

            j++;

        next[i] = j;

    }

    return next;

}

 

// KMP搜索函数

void kmpSearch(string s, string p) {

    // 计算模式串p的next数组

    vector<int> next = computeNext(p);

    // 文本串s的长度

    int n = s.size();

    // 模式串p的长度

    int m = p.size();

    // 初始化文本串和模式串的指针

    for (int i = 0, j = 0; i < n; i++) {

        // 当j大于0且当前字符不匹配时

        while (j > 0 && s[i]!= p[j])

            // j更新为next[j - 1]

            j = next[j - 1];

        // 如果当前字符匹配

        if (s[i] == p[j])

            // j自增1

            j++;

        // 如果模式串完全匹配

        if (j == m) {

            // 输出匹配位置

            cout << "匹配位置:" << i - m + 1 << endl;

            // j更新为next[j - 1],准备下一次匹配

            j = next[j - 1];

        }

    }

}

 

int main() {

    // 定义文本串和模式串

    string s = "ABABABCAA", p = "ABABC";

    // 调用KMP搜索函数

    kmpSearch(s, p);

    return 0;

}

 

 

在这段代码中, kmpSearch  函数负责在文本串  s  中查找模式串  p  。它先调用  computeNext  函数计算出模式串的next数组,然后在匹配过程中,根据next数组来调整模式串的匹配位置,从而实现高效的字符串匹配。

 

四、考研模拟题实战(附解析)

 

问:给定文本串  s="ababaababac"  和模式串  p="ababac" ,求:

 

1.模式串的next数组;

 

2.所有匹配起始位置。

 

解析

 

next数组计算:

 - 模式串  p = a b a b a c  。

 - 计算过程如下:

 - 第一个字符  a , next[0]=0  。

 - 前两个字符  ab ,无公共前后缀, next[1]=0  。

 - 前三个字符  aba ,最长公共前后缀是  a , next[2]=1  。

 - 前四个字符  abab ,最长公共前后缀是  ab , next[3]=2  。

 - 前五个字符  ababa ,最长公共前后缀是  aba , next[4]=3  。

 - 前六个字符  ababac ,无公共前后缀, next[5]=0  。

 - 所以  next = [0,0,1,2,3,0]  。

 

匹配过程:

 - 从文本串的第一个字符开始和模式串匹配。

 - 当匹配到模式串的第5个字符(字符  c  )时,和文本串对应位置不匹配。此时,根据  next[4]=3  ,将模式串右移  5-3 = 2  位继续匹配。

 - 最终在文本串的位置5找到完全匹配。

 

五、KMP的优化与扩展

 

- Nextval数组:在一些情况下,模式串中存在重复字符,使用next数组可能会导致不必要的比较。Nextval数组对next数组进行了优化,避免了这些冗余匹配,进一步提高了匹配效率。

 

- 应用场景:KMP算法在很多实际场景中都有广泛应用。比如在生物信息学中,可以用于DNA序列匹配,快速找到特定的基因片段;在编译器开发中,能够实现关键字识别,提升编译效率。

 

总结

 

KMP算法的关键就在于巧妙地利用已经匹配过的信息,减少匹配过程中的回溯操作。通过深入理解next数组的原理和计算方法,我们就能轻松解决大部分字符串匹配问题。建议大家在学习过程中,结合文末给出的考研真题,自己动手用代码模拟一遍匹配过程,这样能更好地掌握KMP算法。

 

参考资料

 

- 数据结构考研真题解析

-《数据结构 C语言版》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值