KMP中求解next数组的代码的分析

目录

next数组的含义:

普遍的代码:

暴力解法:

小插曲:可以优化的可能性

next数组的优化解法:

优化过程:

第一步优化:

第二步优化:


next数组的含义:

next数组是[1,i]这个区间的最长公共前后缀的长度,所以,求解next数组,其实就是求出模式串的每个区间对应的最长公共前后缀的长度

普遍的代码:

这个代码是常见的next数组求解的代码,这也是一个优化度非常高的代码,所以它很优美、好使,但是也费解;所以我尝试从next数组的暴力求法,通过优化得到下面的代码,来帮助大家理解

for (int i = 2, j = 0; i <= m; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j ++ ;
    ne[i] = j;
}

暴力解法:

以[1,5]区间为例,用j来表示最长公共前后缀长度,如果[1,4]与[2,5]匹配,则最长公共前后缀长度j=4,如果[1,3]与[3,5]匹配,则最长公共前后缀长度j=3,后面以此类推。通过这样逐步缩短前后缀的方法,我们总能找到最长的公共前后缀。假设,我们现在得出[1,5]这个区间内最长公共前后缀是[1,3]=[3,5],则j=3,即next[5]=3

小插曲:可以优化的可能性

对于一个通过遍历(或者类似于遍历的操作)来解决问题的算法,如果当前遍历的数据和上次遍历的数据之间存在交集,则这个算法大概率可以被优化,此时我们通过上次遍历,已经获得了当前待遍历的数据的大部分信息,利用这些信息,找出这些数据之间的关系,利用数据之间的关系,来优化算法

next数组的优化解法:

承接上面next[5]=3的结论,假设现在要计算[1,6]这个区间内的最长公共前后缀的长度,是否还需要用暴力解法,通过逐步缩短前后缀的方式,得到最长公共前后缀呢?答案是是否定的。
因为在在求解[1,6]范围内的最长公共前后缀时,我们用到的数据和求解[1,5]范围内的最长公共前后缀长度的数据之间存在交集(而且交集内元素有很多),此时我们不妨先挖掘一下这些数据彼此之间的关系,看能否减少暴力解法中逐步缩短前后缀的次数

优化过程:

第一步优化:不要每次都从最长的前后缀开始匹配

如图所示,由于在求[1,5]的最长公共前后缀长度时,我们已经知道[1,4]和[2,5]已经不匹配了,所以显然,在求[1,6]的最长公共前后缀长度时,[1,5]和[2,6]是不需要再去验证的;此时只需要验证[1,4]和[3,6]就OK了

并且,因为我们知道next[5]=3即[1,3]和[3,5]是匹配的,所以直接验证p[4]和p[6]是否相等,也就是p[j+1]和p[6](此时的j存储的是[1,5]范围内的最长公共前后缀长度)或者是p[next[5]+1]和p[6]是否相等即可

大家可以自行尝试多几次这样的例子,很容易发现规律,就是每次当我要求出[1,i]范围内的最长公共前后缀时,第一个需要验证的前后缀是[1,j+1]和[i-j,i](j此时存储的是[1,i-1]区间的最长公共前后缀长度),而验证这个区间,只需要比较p[j](相当于p[next[i-1]])和p[i]是否相等,如果相等,则next[i]自然就是j+1;这也就是代码里

if(p[i]==p[j+1])j++;
next[i]=j;

的含义

到这里其实已经比原来的暴力解法优化很多了,至少我们不需要每次都从最长的那个前后缀开始试探了;但是还有一个问题,如果p[i]不等于p[j+1]的话,我们是不是又要像暴力解法一样,通过逐步缩短前后缀来找到最长公共前后缀呢?

第二步优化:最高效地缩短前后缀

对于上面的问题,答案是否定的。还是接着上面[1,6]的例子,当p[4]不等于p[6]的时候,按照暴力解法的思路,我们需要把前后缀缩短到[1,3]和[4,6],但是如果我们能通过之前的数据,判断[1,2]和[4,5]这个区间不能匹配的话,就完全没有必要去判断[1,3]和[4,6]是否匹配,因为答案是显然的。

此时我们的目的是通过之前已有的数据判断[1,2]和[4,5]是否匹配,此时我们应该注意到,next[5]=3除了告诉我们[1,3]和[3,5]匹配以外,还告诉了我们[2,3]和[3,4]匹配,如此问题变转化为了判断[1,2]和[2,3]是否匹配

我们可以很快反应到,这个问题其实在求解[1,3]这个区间的最长公共前后缀的时候就被解决了,因为如果next[3]=2的话,[1,2]和[2,3]显然是匹配的;但是,如果next[3]=1(next[3]的结果只能是0,1,2里取)的话,[1,2]和[2,3]显然是不匹配的,这样我们就完全没有必要去判断[1,3]和[4,6]是否匹配,因为此时我们很清楚[1]和[5]是匹配的,我们直接判断[1,2]和[5,6]是否匹配,即判断p[2]和p[6]是否相等即可

综合上一段的分析,我们不难发现,选择[1,3]和[4,6]去匹配还是选择[1,2]和[5,6]去匹配,其实取决于next[3]也就是next[j]。换言之,我们要选择匹配的区间应该是[1,next[j]+1]和[6-next[j],6],也就是比较p[next[j]+1]和p[6]是否相等

那么为了用循环实现这个操作,当发现[1,4]和[3,6]不匹配即p[4]不等于p[6]的时候,我们直接让j=next[j],那么下次循环进行的时候也就变成了比较p[j+1]和p[6]是否相等,即上一段想实现的比较p[next[j]+1]和p[6]是否相等,如果相等,跳出循环,经过下一条if的时候,就有next[6]=j++,否则就重复上述操作,继续以优化的解法缩短前后缀,而不是用暴力的方法逐步缩短前后缀
这也就是这条代码的含义:

while (p[i] != p[j + 1]) j = ne[j];

当然,前后缀肯定不能无限缩短,当j==0即前后缀长度为零时,此时就不能再继续缩短了,如果这时p[6]又刚好不等于p[1]的话,那些跳出循环到下一个if时,next[6]就只能是零了。即j为零时循环也要结束,修改代码如下

while (j && p[i] != p[j + 1]) j = ne[j];
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值