图解KMP算法(C++实现)

需求提出
给定一个模式串 pattern = “ddywabcdababcdabd” 和一个子串 substr = “abcdabd”,需判断 pattern 中是否包含 substr,如果包含,返回 substr 第一次在 pattern 中出现的位置;如果不包含,返回 -1(常用手段)

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;
/*
* Author: 酒馆店小二
* Description: kmp算法学习
* Date: 2022-2-17 22:41:52 星期四
* FileName: kmp.cpp
* Location: D:\VSCODE_CPP\algorithm\kmp\kmp.cpp
*/

vector<int> kmpNext(const string &substr) {  // KMP next 数组
    int n = substr.size();
    vector<int> next(n);
    next[0] = 0;
    for (int i = 1, j = 0; i < n; ++i) { // 注意 i 从1开始
        // 要保证 j > 0,因为会用到 j - 1 做下标
        while (j > 0 && substr[i] != substr[j]) { // 前后缀不相同
            j = next[j - 1]; // 向前回退
        }
        if (substr[i] == substr[j]) { // 找到相同的前后缀
            j++;
        }
        next[i] = j; // 将 j(前缀的长度) 赋给next[i]
    }
    return next;
}

int kmpMatch(const string &pattern, const string &substr) {  // KMP匹配法
    if (substr.size() == 0) { // 当 substr 为空时应该返回 0
        return 0;
    }
    int pLen = pattern.size();
    int sLen = substr.size();
    vector<int> next = kmpNext(substr);
    
    for (int i = 0, j = 0; i < pLen; i++) {
        // 仍然要保证 j > 0,因为会用到 j - 1 做下标
        while (j > 0 && pattern[i] != substr[j]) {
            j = next[j - 1]; // 注意此处,寻找上一个字符对应的匹配表的位置
        }
        if (pattern[i] == substr[j]) { // 匹配,j 加 1
            j++;
        }
        if (j == sLen) { // 模式串中出现了子串
            return i - j + 1;
        }
    }
    return -1;
}

int main(int argc, char *argv[]){
    string pattern = "ddywabcdababcdabd";
    string substr = "abcdabd";
    cout << "Match position is: " << kmpMatch(pattern, substr) << endl;

    return 0;
}

在这里插入图片描述

赶时间的小伙伴可以拿去应急了。

第一章 暴力匹配实现

在这里插入图片描述
第一次匹配不符合后,子串向后移一位,从头再开始匹配
在这里插入图片描述
省略若干次移动。。。
就这样一位位的匹配、一位位的移动,最后当 j == substr.size() 时,匹配结束
ps: 当最后一位 d 匹配时,j 会再加一位,代码中的 ++j,所以 j == substr.szie()
在这里插入图片描述

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;
/*
* Author: 酒馆店小二
* Description: kmp算法学习
* Date: 2022-2-17 22:41:52 星期四
* FileName: kmp.cpp
* Location: D:\VSCODE_CPP\algorithm\kmp\kmp.cpp
*/

int violentMatch(const string &pattern, const string &substr) {
    int pLen = pattern.size();
    int sLen = substr.size();
    int i = 0, j = 0; // i、j 分别指向 pattern 和 substr 的下标
    while (i < pLen && j < sLen) {
        if (pattern[i] == substr[j]) { // 当前字符匹配成功
            i++;
            j++;
        } else { // 当前字符匹配失败
            i = i - j + 1;
            j = 0;
        }
    }
    if (j == sLen) { // 匹配成功,返回子串在模式串中的位置,否则返回 -1
        return i - j;
    } else {
        return -1;
    }
}

int main(int argc, char *argv[]){
    string pattern = "ddywabcdababcdabd";
    string substr = "abcdabd";
    cout << "Match position is: " << violentMatch(pattern, substr) << endl;

    return 0;
}

在这里插入图片描述
上面的程序是没有问题的,但不够好!(正如高中时候数学老师的一句话:我不能说你错,只能说你不对~~~)
上述程序的时间复杂度是O(mn),m、n是两个字符串的长度。聪明的程序员们是无法接受这样的时间开销的,于是就有了KMP算法的产生。

第二章 KMP历史渊源

Knuth-Morris-Pratt 算法,由 Donald Knuth、James H. Morris 和 Vaughan Pratt 三人于 1977 年联合发表,因此人们称他为克努特—莫里斯—普拉特操作(简称KMP算法)(所谓名垂青史就是这么个情况)。KMP主要应用在字符串匹配的场景中,其主要思想是当出现字符串不匹配的情况时,记录之前部分已经匹配的文本内容,利用 next 数组避免从头匹配,从而提高匹配效率,next 数组是KMP算法的核心和难点。KMP算法的时间复杂度O(m+n),而使用暴力匹配的时间复杂度则是O(mn),匹配规模越大,KMP算法的优势越明显。

第三章 KMP算法原理

1、首先,用 pattern 的第一个字符和 substr 的第一个字符去比较,不符合,substr 向后移动一位在这里插入图片描述
2、比较二者字符,不符合,后移。
在这里插入图片描述
3、比较二者字符,不符合,后移。
在这里插入图片描述
4、直到遇到匹配的元素,于是双方的下标向后移动,遇到 pattern 有一个字符与 substr 对应的字符不符合。在这里插入图片描述
5、此时按照暴力的思想是将子串向后移一位,再从头比较(亏损做法)在这里插入图片描述
6、其实这样很不划算,因为此时”abcdab”已经比较过了,我们知道了
pattern[5] = substr[1] = b,但是后移一位的效果是 pattern[5] 跟 substr[0] 比较,而
substr[0] != substr[1],所以肯定匹配失败。KMP算法的想法是,设法利用这个已知信息,不要把”搜索位置“移回已经比较过的位置,而是继续把他(子串下标)向后移,这样就提高了效率。怎么做到把刚刚重复的步骤省略掉?可以对 substr 计算出一张《匹配表》,这张表的产生在后面介绍。

substrabcdab
a000012

7、此时需要看子串已匹配字符的前一个字符对应的下标,此处是模式串的 a 和子串的 d 不匹配,于是需要看子串前一个字符在匹配表中对应的下标,也就是 b 的下标,根据查表可以得知,b 下标应该是2。这是什么意思呢?— 就是说,模式串不动,子串移动到下标为2的位置来与模式串匹配,暴力匹配是模式串的 a 与子串的 d 比较,KMP算法是模式串的 a 与子串的 c 比较,可以发现,模式串并没有回溯较多位置,子串也没有从头比较。
在这里插入图片描述

8、模式串的 a 与子串的 c 不匹配,于是我们查看此时子串的前一个字符在匹配表中对应的下标,即 b 对应的下标,为 0,意思就是模式串不动,将子串的下标移到 0 处,挨着挨着比较,最终匹配成功。返回结果是10(因为从模式串的下标10的位置处第一次出现了完整的子串)。在这里插入图片描述
KMP 算法如此高效,匹配表功不可没,俗称:next 数组。next 数组的介绍如下。

第四章 KMP的匹配表(next数组)

介绍匹配表如何产生之前,我们首先介绍什么是前缀什么是后缀?

  • 什么是前缀:包含首字母但不包含尾字母的所有子串。
  • 什么是后缀:包含尾字母但不包含首字母的所有子串。

这里以模式串“abcdab”为例,该模式串的前缀和后缀依次如下表:

abcdab前缀(从前往后数)后缀(从后往前数)最大相同子串长度
a0
abab0
abca, abc, bc0
abcda, ab, abcd, cd, bcd0
abcdaa, ab, abc, abcda, da, cda, bcda1
abcdaba, ab, abc, abcd, abcdab, ab, dab, cdab, bcdab2

于是匹配表就做好了,使用方法见前面。

substrabcdab
a000012

说个关于匹配表的问题:next 数组中的数字表示的是什么,为什么要这么表示?
这里问的就是匹配表的定义了:记录下标 i (包括 i之前的字符串中有多长的相同前缀;匹配表的任务是当前位置匹配失败后,找到之前已经匹配的位置再重新匹配,这也就意味着在某个字符匹配失败时,匹配表会告诉你下一步匹配时子串应该跳到哪个位置。(这里的关键是理解之前已经匹配过的字符位置,强烈建议读者把匹配表打印出来,加深理解)

第五章 KMP算法代码实现

代码随想录的作者 carl 大佬在b站有视频:
理论篇:https://www.bilibili.com/video/BV1PD4y1o7nd
代码篇:https://www.bilibili.com/video/BV1M5411j7Xx

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;
/*
* Author: 酒馆店小二
* Description: kmp算法学习
* Date: 2022-2-17 22:41:52 星期四
* FileName: kmp.cpp
* Location: D:\VSCODE_CPP\algorithm\kmp\kmp.cpp
*/

vector<int> kmpNext(const string &substr) {  // KMP next 数组
    int n = substr.size();
    vector<int> next(n);
    next[0] = 0;
    for (int i = 1, j = 0; i < n; ++i) { // 注意 i 从1开始
        // 要保证 j > 0,因为会用到 j - 1 做下标
        while (j > 0 && substr[i] != substr[j]) { // 前后缀不相同
            j = next[j - 1]; // 向前回退
        }
        if (substr[i] == substr[j]) { // 找到相同的前后缀
            j++;
        }
        next[i] = j; // 将 j(前缀的长度) 赋给next[i]
    }
    return next;
}

int kmpMatch(const string &pattern, const string &substr) {  // KMP匹配法
    if (substr.size() == 0) { // 当 substr 为空时应该返回 0
        return 0;
    }
    int pLen = pattern.size();
    int sLen = substr.size();
    vector<int> next = kmpNext(substr);
    
    for (int i = 0, j = 0; i < pLen; i++) {
        // 仍然要保证 j > 0,因为会用到 j - 1 做下标
        while (j > 0 && pattern[i] != substr[j]) {
            j = next[j - 1]; // 注意此处,寻找上一个字符对应的匹配表的位置
        }
        if (pattern[i] == substr[j]) { // 匹配,j 加 1
            j++;
        }
        if (j == sLen) { // 模式串中出现了子串
            return i - j + 1;
            // 暴力匹配与此处不一致是因为,前者在已经得到匹配字符串的时候,i和j同时+1,后者只有j + 1
            // 所以此时 i 要 + 1 再减 j
        }
    }
    return -1;
}

int main(int argc, char *argv[]){
    string pattern = "ddywabcdababcdabd";
    string substr = "abcdabd";
    cout << "Match position is: " << kmpMatch(pattern, substr) << endl;

    return 0;
}

在这里插入图片描述
kmp练手题:
力扣459:重复的子字符串
力扣28:实现strStr()

最后修改于:2022.2.19
最是人间留不住,朱颜辞镜花辞树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值