KMP算法详解

什么是KMP算法? 

有句话可以这么形容KMP:一个人能走的多远不在于他在顺境时能走的多快,而在于 他在逆境时多久能找到曾经的自己。

KMP算法是一个字符串匹配算法,取得是三个发明人的名字首字母。KMP算法的作用 是在一个已知字符串中查找子串的位置,也叫做串的模式匹配,后文主串和模式串匹配, 子串和模板串匹配。

暴力做法

KMP可以解决串的模式匹配,但是一般我们解决这个问题首先想到的是暴力做,什么 也不管,直接两个for循环。暴力匹配也叫朴素的模式匹配。

从主串和子串的第一个字符开始,将两字符串的字符一一比对,如果出现某个字符 不匹配,主串回溯到第二个字符,子串回溯到第一个字符再进行一一比对。如果出 现某个字符不匹配,主串回溯到第三个字符,子串回溯到第一个字符再进行一一比对… 一直到子串字符全部匹配成功。最坏的情况下时间复杂度是O(m*n),这样不停的回溯速 度会非常慢,当数据非常大工程量也会非常大。

所以这时候就要想想怎么优化了,再想一下暴力做法,一直在回溯,回溯的步骤太多了, 所以能不能找到一种规律减少回溯的次数,这就是KMP算法

KMP算法

KMP算法一种改进的模式匹配算法,是D.E.Knuth、V.R.Pratt、J.H.Morris于1977年联合 发表,KMP算法又称克努特-莫里斯-普拉特操作。

它的改进在于:每当从某个起始位置开始一趟比较后,在匹配过程中出现失配,不 回溯i,而是利用已经得到的部分匹配结果,将一种假想的位置定位“指针”在模式上 向右滑动尽可能远的一段距离到某个位置后,继续按规则进行下一次的比较

比如当我们在主串下标为3匹配子串,往后继续匹配主串,在下标是10的位置主串和 子串不匹配,这个时候就要把子串往后移动到首字母相同的位置继续匹配,但其实我 们中间已经匹配了很多字符了,里面是有一些额外信息在里面的。我们利用这些额外 信息就可以帮我们少枚举一些东西。

还是以上面为例,当我们在下标10主串和子串不匹配,我们要从3往后面找和子串首 字母相同的位置然后继续匹配,如果这个位置在10前面的话,我们可以发现这个位置 到10的距离和第一次匹配的子串有重合的部分,我们假设这个位置是6

我们可以很明显的发现6-10的数组在第一次匹配的子串和第二次匹配的子串出现重合 的情况,也就是在子串中间和前面是重合的,所以我们只要将子串直接移动到下标为6 的位置就行,这样我们就减少了枚举的次数。所以我们只要求出子串往后移动多少位 可以出现重合即可

这个问题只和我们的子串也就是模板串有关系,如果我们可以预处理出来这种子串的中 间和开头有一段距离相同,我们求出这一段的最大值就行,这一段的距离越大的话说明 子串往后移动的越少

所以要给我们的子串,也就是模板串的每个点进行预处理,以某个点为终点的后缀和前 缀相等,相同的最大长度是多少,这个就是KMP算法中最难的next数组的含义。

next[i]表示的就是以i为终点的后缀和从1开始的前缀相等,且相同的部分最长,这里 我们默认子串下标从1开始。比如next[i]=j就表示在子串中p[1,j]=p[i-j+1,i],这里我们 用p数组暂时表示子串,这个就表示子串中下标从1到j这一段和i-j+1到i是相等的, 而且长度最长。所以下次从j+1再开始继续匹配。

next数组

next 数组的值是除当前字符外(注意不包括当前字符)的公共前后缀最长长度

求next数组最重要的一点是找最长公共前后缀,什么是前后缀呢

前缀是除了最后一个字符的所有子串。

后缀是除了第一个字符的所有子串。

如图

举个栗子

比如子串是ababababab,我们求他的next数组,子串下标从1开始

很明显next[1]=0,因为第一个默认是0

next[2]=0,因为没有公共前后缀

next[3]=1,最长公共前后缀是a

next[4]=2,最长公共前后缀是ab

Next[5]=3,最长公共前后缀是aba,依次类推next[6]=4.....

我们可以发现next数组的值就是子串退回时的下标

动画演示

我们可以看一下暴力做法和KMP做法的一个区别

例题

给定一个模式串 SS,以及一个模板串 PP,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模板串 PP 在模式串 SS 中多次作为子串出现。

求出模板串 PP 在模式串 SS 中所有出现的位置的起始下标。

输入格式

第一行输入整数 NN,表示字符串 PP 的长度。

第二行输入字符串 PP。

第三行输入整数 MM,表示字符串 SS 的长度。

第四行输入字符串 SS。

输出格式

共一行,输出所有出现位置的起始下标(下标从 00 开始计数),整数之间用空格隔开。

数据范围

1≤N≤1051≤N≤105
1≤M≤1061≤M≤106

输入样例:

3
aba
5
ababa

输出样例:

0 2

代码实现

#include <iostream>
using namespace std;
const int N=1e7+10;
char p[N],s[N];
//next数组表示的是不匹配时子串下标退回的位置
//比如next[i]=j就表示在子串中p[1~j]=p[i-j+1~i]
//所以子串直接退回到j下标继续和主串进行模式匹配直到匹配成功为止
int ne[N];
int n,m;
int main()
{
    cin>>n>>p+1>>m>>s+1;
    //求next数组的过程
    //类似于KMP匹配过程
    for(int i=2,j=0;i<=n;i++)
    {
        while(j&&p[i]!=p[j+1]) j=ne[j];
        if(p[i]==p[j+1]) j++;
        ne[i]=j;
    }
    //KMP匹配过程
    for(int i=1,j=0;i<=m;i++)
    {
        //如果子串没有退回到起点,并且主串和子串不匹配
        while(j&&s[i]!=p[j+1]) j=ne[j];//j退回到重合部分的终点位置
        if(s[i]==p[j+1]) j++;
        //匹配成功
        if(j==n)
        {
            cout<<i-n<<" ";
            j=ne[j];
        }
    }
    return 0;
}

 思路

匹配操作就不用说了,next数组的求法和匹配的操作几乎一模一样,通过子串自己和自己进行匹配操作出来的

假设[m,n]是相同的,即next[i-1]=j,如果此时p[i]!=p[j+1],在进行到这步操作时前面的next 的值已经求好了,所以我们要再ne一下,把j退到next[j]的位置,即next[j],退完之后 我们再看p[ne[j]+1]和p[j+1]是否匹配,不匹配的话在ne一下,把j继续退到next[j]的位 置,也就是ne[ne[j]],这样循环直到最后。

最后, 我写的也不是很好,有很多想说的也没有完全表达出来,但是我尽力将我对KMP算法的知识点总结了下来,自己对KMP也有了一个更深刻的印象,希望对读者也有所帮助,如果对next数组还不太懂的可以参考一下这个视频「天勤公开课」KMP算法易懂版_哔哩哔哩_bilibili

这个视频我感觉还是比较浅显易懂的,而且还有动画演示,讲的很容易听懂。 

  • 22
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

真的没事鸭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值