KMP算法详解

KMP算法是一种高效的字符串匹配算法,通过next数组减少回溯次数,提高匹配效率。文章详细介绍了KMP的原理,包括next数组的计算方法和匹配过程,并给出了代码实现。动画演示和例题进一步解释了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

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

我们这里说的KMP不是拿来放电影的(虽然我很喜欢这个软件),而是一种算法KMP算法是拿来处理字符串匹配的。换句话说,给你两个字符串,你需要回答,B串是否是A串的子串(A串是否包含B串)。比如,字符串A="I'm matrix67",字符串B="matrix",我们就说B是A的子串。你可以委婉地问你的MM:“假如你要向你喜欢的人表白的话,我的名字是你的告白语中的子串吗?” 解决这类问题,通常我们的方法是枚举从A串的什么位置起开始与B匹配,然后验证是否匹配。假如A串长度为n,B串长度为m,那么这种方法的复杂度是O (mn)的。虽然很多时候复杂度达不到mn(验证时只看头一两个字母就发现不匹配了),但我们有许多“最坏情况”,比如,A= "aaaaaaaaaaaaaaaaaaaaaaaaaab",B="aaaaaaaab"。我们将介绍的是一种最坏情况下O(n)的算法(这里假设 m<=n),即传说中的KMP算法。 之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。这时,或许你突然明白了AVL 树为什么叫AVL,或者Bellman-Ford为什么中间是一杠不是一个点。有时一个东西有七八个人研究过,那怎么命名呢?通常这个东西干脆就不用人名字命名了,免得发生争议,比如“3x+1问题”。扯远了。 个人认为KMP是最没有必要讲的东西,因为这个东西网上能找到很多资料。但网上的讲法基本上都涉及到“移动(shift)”、“Next函数”等概念,这非常容易产生误解(至少一年半前我看这些资料学习KMP时就没搞清楚)。在这里,我换一种方法来解释KMP算法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

真的没事鸭

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

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

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

打赏作者

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

抵扣说明:

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

余额充值