最通俗易懂的KMP算法原理


前言

其实这一期本来是打算更新一期图算法的,但是作为数据结构的助教,还是想记录和数据结构有关的一些东西,也可以理解为我想让更多的人看我写的一些东西。反正,总而言之,不管是为了什么,这一期给大家通俗易懂的讲解一下KMP算法,依旧坚持我们的宗旨,用最通俗易懂的话语来解释算法的本质!

如果本文有错误,还请大家及时纠正!


一、KMP算法的本质

大家先不要慌,这里并不给大家讲那些复杂的原理,主要给大家讲一下KMP算法的流程,也就是这个算法是怎么工作的以及它为什么比暴力法要优。

首先介绍一下KMP算法的作用吧,KMP算法主要应用在串匹配问题中,也就是给你一个很长的字符串P,然后给你一个目标字符串Q,现在让你在字符串P中找到字符串Q出现的位置。
在这里插入图片描述
在面对上面的问题的时候,传统的暴力法原理很简单,使用两个下标i,j分别指向两个字符串P和Q,一开始都从第一个字符开始逐个比较。

如果相同,则同时后移一位进行下一个字符的比较。

如果遇到一个不相同的字符,则将Q的下标j置零,也就是从头开始比较,而P的下标则重置到开始比较时的下一个位置。

参考代码如下

int getIndex(string p,string q)
{
    int i=0;
    int j=0;
    while(i<(int)p.length() && j<(int)q.length())
    {
        if(p[i] == q[j])
        {
            i++;
            j++;
        }
        else
        {
            i=i-j+1;
            j=0;
        } 
        if(j==q.length())
            return i-q.length();
    }
    return -1;
}

在讲解KMP算法的本质时,首先引入字符串的前缀和后缀的概念,下面是我对于前缀和后缀的个人理解。

前缀:从字符串第一个字符开始的任意长度的连续子串
后缀:从字符串最后一个开始往前的任意长度的连续子串

举个例子,对于字符串“ababc",前后缀如下所示。

前缀“”(空串),“c”,“bc”,“abc”,“babc”,“ababc”
后缀“”(空串),“c”,“bc”,“abc”,“babc”,“ababc”

之后引入最长相同前缀后缀,顾名思义,就是对于一个字符串来说,可以列出上面的前缀和后缀集合,那么最长相同前缀后缀就是前缀字符串集合和后缀字符串集合的并集中的最长的字符串。

例如对于字符串"eefegeef"而言,最长相同前缀后缀为“eef”,长度为3。也可以通过下图来理解,即两个字符串的最长相交长度。
在这里插入图片描述
KMP算法其实就是依靠最长相同前缀后缀进行比较的,next数组中存的值其实就是最长相同前缀后缀的长度。在进行字符串匹配时,如果P[i] != Q[j],其实这也是第一次不匹配, 这说明了之前的j-1个字符都匹配上了,对于这j-1个字符构成的字符串S,我们只需要从S的最长相同前缀后缀的长度处开始下一轮比较即可,不需要再回退i。

怎么理解上面的本质呢,首先可以肯定是前j-1个字符是匹配的,设前j-1个字符的最长相同前缀后缀的长度为n,则可以知道对于这j-1个字符构成的字符串S来说,后面n个字符是和原串P相匹配的,且后面n个字符与S的前n个字符也是相同的,所以可以直接把前缀的n个长度部分放到这里来,放过来之后依旧是匹配的

举个例子吧
在这里插入图片描述
从上面可以看出,"abca"的最长前缀后缀为“a”,长度为1,所以直接从Q[1]和P[i]开始比较即可。
在这里插入图片描述
上图中恰好Q[1] = P[i],那么如果不相同的话,则继续上面找最长相同前缀后缀的过程,继续平移字符串。如果j-1个字符构成的子串的最长相同前缀后缀的长度为0,则Q从第一个字符开始比较,P则从第i+1个字符开始比较。

二、KMP核心算法

根据前面对于KMP算法本质的分析,可以看出KMP算法的核心在于求出每个前缀的最长相同前缀后缀。在KMP算法中,这个集合被称作next数组。所以求出next数组就是整个KMP算法的核心步骤。

根据前面的分析,next[j]的值应该是前j-1个字符构成的字符串的最长相同前缀后缀的长度。
在这里插入图片描述
根据上述公式,可以知道如何求出next数组,但是这种计算方法过于繁琐,每一次都要求一次最长相同前缀后缀的长度。这里我们可以利用KMP算法的本质思想来推导next数组。具体思路如下

假设next[j] = k,那么说明Q(0)…Q(k-1) = Q(j-k)…Q(j-1),那么接下来,如果Q[k] = Q[j],那么很明显可以得出next[j] = k+1。如果Q[k] != Q[j],这里使用KMP算法的思想,令k=next[k],然后再去判断Q[k]与Q[j]的关系。
在这里插入图片描述
从上表中可以看出,当第一次Q[k] != Q[j]时,就变成了第三行和第一行的对比,类似于之前的第一行和第二行的一个比较。关键就在于k=next[k],充分利用了KMP算法的思想,对于Q(0)…Q(k-1),最长相同前缀后缀长度为next[k],但是由于Q[k] != Q[j],所以next[j] != k+1。

这里根据KMP算法的思想,由于Q(0)…Q(k-1)的最长相同前缀后缀为Q(0)…Q(next[k]-1),所以也可以将这条串与原本的串进行比较,因为这个也是原本的串的一个相同前缀后缀,只不过不是最长的。利用这一点,进行next[j]的计算。

下面给出next数组的计算代码

void getNext(int *next,string q)
{
    int j=0;
    int k=-1;
    next[j] = k;
    while(j<(int)q.size())
    {
        if(k==-1 || q[j] == q[k])
        {
            j++;
            k++;
            next[j]=k;
        }
        else
        {
            k=next[k];
        }
        
    }
}

三、KMP匹配

在前面计算好next数组的值之后,就可以开始进行字符串匹配了,具体匹配方法完全参考第一部分KMP算法的本质,只是第一部分没有引入next数组,但是根据第二部分可知,next数组记录的就是最长相同前缀后缀的长度。具体到代码实现的时候稍微注意一些细节就可以了。

匹配部分代码如下所示。


int KMP(string p,string q)
{
    int *next = new int[(int)q.size()];
    getNext(next,q);
    int i=0;
    int j=0;
    while(i<(int)p.size() && j<(int)q.size())
    {
        if(j==-1 || p[i] == q[j])
        {
            i++;
            j++;
        }
        else
        {
            j=next[j];
        }
    }
    if(j==(int)q.size())
        return i-j;
    return -1;

}

总结

本次讲KMP我可能更侧重的是讲解KMP算法的一个思想,并不太会侧重讲解如何实现KMP算法,实现KMP算法的方法有很多,这里的样例代码是next[0]=-1,其实也可以next[0]=0,各有各的写法,但是最终的思路和思想都是一样的。

最后,这里的文章复制与我自己公众号写的推文,法苏eve每周都会更新一些有趣有用的编程小知识,有兴趣的小伙伴可以关注一下,点关注不迷路哟!
在这里插入图片描述

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值