kmp算法为什么要求最长相等前后缀的长度做next数组?

kmp基础-前情提要

网上讲kmp算法的视频一抓一大把,但占绝大多数的是在讲如何根据模式串的前后缀匹配算出next数组和求next数组的代码讲解。

都会讲解到如下问题:什么是前缀,什么是后缀,怎样才是前缀和后缀匹配相等,kmp复杂度O(n+m)啦

        在这些视频中,我们首先都知道了求next数组所关心的前缀是相对于模式串(也就是上图ababc串)的子串(如上图左侧一排的a,ab,aba,abab等)去研究的。然后对于这些子串再检查其是否有前后相等的前后缀,有的话是几就是next数组对应下标的值了。在这方面众多视频讲解得比这寥寥几句好多了,可以再b站搜索串匹配等关键字观看学习。

        但都没有直接了当地点破一个出发前提:我们为什么可以根据模式串的这些前缀串的前缀和后缀相等推出后一位失配时要回移到的位置?

以下是在视频评论区找到的非常形象的评价:

该视频,好比做一道动态规划题,不经思考的直接得到了动态转移方程,然后跟着动态转移方程写出了代码,却没有讲我们是怎么得到动态转移方程的

不过,笔者在经过灵光乍现和算法实现后,发现其实就是捅破一层窗户纸的事,可能大多up主没有想到咱们没想通这点吧(ಥ_ಥ) ,

        笔者发现代码随想录的kmp算法讲解很不错,要点全面内容易于理解,0基础多听两遍也都会了,推荐给大家。并且其中也讲到了本文中心问题“为什么可以根据模式串的这些子串的前缀和后缀相等推出失配时要回移到的位置”

        我的理解是:因为相等意味着改后缀是已经走(匹配)过的路,通过直接跳到相等前缀之后,可以不用重复匹配做功,这便是kmp算法比传统算法优化的关键。也可以类比前缀和数组理解,毕竟next数组其实存的也就是前缀表嘛。

ps:Next数组有多种存储格式,依据推导出的算法不同,对应的Next数组呈现的格式也不同,代码随想录的视频里有详细介绍。

为什么求最长相等前后缀的长度(注意:此处的next数组整体右移版,即首位固定-1,剩余为原始前缀表右移一位对应得出的结果)

        

移前图
移后图

由图1图2的变化应该可以看出,关键就是P下标为0的前缀 和 P下标为2的后缀是相等的,所以可以利用而不用再从T下标为1的值接着比,可以直接跳到T下标为3处接着比较,不用像暴力一样从T的0,1,2才到3。使用整体右移版next数组可以简单又最符合理性,遇到不匹配p[i]的情况,直接拿对应的next[i]接着比较,不用再加一减一,就,很舒服( • ̀ω•́ )✧

构成前缀表代码:

ps: (不是上图的 next数组,而是更原始的前缀表,如需变为上图 next 还需将 perfix 整体右移且首位为-1;此处perfix前缀表首位为0,遇到不匹配时需要取 perfix [ j - 1 ] )

//用双指针表示前后缀,实现O(m)生成前缀表存入next数组
perfix[0]=0;
j=0;//前缀长度,也是每次循环后放入next数组的动态前缀长度,其值的变化有记录的功能

for(int i=1;i<s.size();++i){    //i为后缀起始指针,同时也是生成next数组的下标位置
    //前后缀不相等的情况
    while(j>0 && s[j]!=s[i]){    //继承之前的前缀大小
        j = perfix[j-1];
    }

    //前后缀相等
    if(s[j] == s[i])j++;    //只用if,因为每轮i只动一次,所以前缀j也只动一次;同时j的变化就是指持续到i的前后缀相等的积累值,这样每轮结束后为next[i]赋值时不用重复计算之前相等的
    //如果还显抽象请模拟一次,或看看代码随想录kmp代码实现集
    
    perfix[i] = j;//将该轮计算好的最长相等前后缀存入对应next[i]
}

如果想舒服点变整体右移版可以在计算时每个i,j都提前一位,perfix改名next且空间开大一格(应该是这样吧,还没试过 |ू・ω・` ))

学完kmp算法的铁子可以做做模板题,以防不会用,一打才知道水平

附上我的题解:

class Solution {
public:
    int strStr(string haystack, string needle) {
        int len = needle.size();
        vector<int> next(len);

        //next数组生成
        int j=0,n=haystack.size();
        for(int i=1;i<len;++i){
            while(j>0 && needle[j]!=needle[i])j=next[j-1];
            if(needle[i] == needle[j])j++;
            next[i]=j;
        }
        //for(int i:next)cout<<i<<" ";
        //cout<<endl;

        //O(n+m)遍历
        j=0;
        int i=0;
        for(;i<n;i++){
            
            while(j && haystack[i] != needle[j]){
                
                // cout<<"i="<<i<<"j="<<j<<" haystack[i] != ne[j] "<<haystack[i]<<" "<<needle[j]<<endl;
                j=next[j-1];
            }
            if(haystack[i] == needle[j]){
                // cout<<"i="<<i<<"j="<<j<<" haystack[i] == ne[j] "<<haystack[i]<<" "<<needle[j]<<endl;
                ++j;
            }
            if(j==len)return i-len+1;
        }
        
        
        return -1;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值