KMP:next数组的三种定义方法

本文详细对比了KMP算法中三种next数组定义形式,包括算法笔记中的最长公共前后缀定义、整体加1的版本以及《算法竞赛》中的区间调整,解释了它们的差异和适用场景,帮助读者更好地掌握KMP算法实现。
摘要由CSDN通过智能技术生成

前言

在学习kmp算法的时候只是一知半解,本来寻思会用就行了,但是刷题的时候经常报数组越界,想看别的同学用不同形式的next数组又不懂了,于是不得不把整个next数组的三种定义形式系统地都看一遍,以加深对kmp的理解.本文着重对比三种定义形式的区别,如果你对kmp算法本身有疑问推荐观看 链接: link
b站有个up搬的油管上的,讲的挺好
另外推荐阅读胡凡老师的<<算法笔记>>的讲解,很清晰也很详细,然后读罗永军老师的<<算法竞赛>>的讲解,如果能看懂,那你就不用看本文了.

第一种

算法笔记对next的定义是next[i]为s[0,i]的最长公共前后缀,前缀的最后一位的下标

  • s是字符串,长度为len,下标0到len-1
  • next的长度也为len,下标也是0-len-1;next[n-1]就是p[0,n-1]最长公共前后缀的下标

代码

void getNext(string p,int len){
        next[0]=-1;//最长公共前后缀不能是字符串本身,p[0]的最长公前后缀长度为0,所以next赋值为-1
        int j=-1;
        for(int i=1;i<len;i++){//求解next[1]~next[len-1]
            while(j!=-1&&p[i]!=p[j+1]){//j是最长公共前后缀最后一位的下标,i指向当前失配的字符,所以要用[j+1]去匹配[i]
                j=next[j];
            }
            if(p[i]==p[j+1])j++;
            next[i]=j;
        }
    }

可以看出,之所以把next[0]定义为-1不是-2或者其他值,是为了在匹配的时候对于下标j=-1的时候,依然可以通过索引next[j+1]访问到next[0]

第二种

在算法笔记定义的基础上,给next数组整体+1;此时 next[i]表示[0,i]最长公共前后缀长度 ,也是也是最长公共前后缀的前缀的下一个位置,所以在匹配的时候不用j+1.只要把上面代码小改一下就行

代码

void getNext(string p,int len){
        next[0]=0;//最长公共前后缀不能是字符串本身,p[0]的最长公前后缀长度为0,所以next赋值为0
        int j=0;//j的初值也要改
        for(int i=1;i<len;i++){//求解next[1]~next[len-1]
            while(j!=0&&p[i]!=p[j]){//用j匹配i就可以了
                j=next[j];
            }
            if(p[i]==p[j])j++;
            next[i]=j;
        }
    }

第三种

<<算法竞赛>>中的定义,在第二种定义的基础上,把数组整体向高位移动一个位置即 next[i]表示[0,i-1]区间的最长公共前后缀的长度,也就是前缀的下一个位置
注意

  • p的区间为[0,len-1],长度为len
  • next的区间为[0,len],长度为len+1,最后一个元素是next[len],表示字符串p整个的最长公共前后缀的长度.要把next设置得长一些以防下标越界

void getNext(string p,int len){//len是数组p的长度,是next的最后一个元素的下标,next比p长1
next[0]=0;
next[1]=0;
//理论上next[0]是用不到的
int j=0;
for(int i=1;i<len;i++){
    while(j&&p[i]!=p[j]){
        j=next[j];
    }
    if(p[i]==p[j])j++;
    next[i+1]=0;//求p[0,i]的最长公共前后缀长度
}
}

注意for循环的终止条件和上面的两种定义一样,next偏移的原因是在给next赋值的时候赋值给了next[i+1]而不是next[i]

对比

项目第一种第二种第三种
p长度lenlenlen
next长度lenlenlen+1
next[i]对应p的区间[0,i][0,i][0,i-1]
next值的含义最长公共前后缀,前缀的最后一个元素的下标最长公共前后缀长度最长公共前后缀长度
当前字符匹配成功的判断条件p[i]==p[j+1]p[i]==p[j]p[i]==p[j]
初始化j=-1,next[0]=-1j=0,next[0]=0j=0,next[0]=next[1]=0

如果p的下标从1开始呢?

在刷题的时候,有时由于各种原因,数组p的戏下标区间为[1,len],长度为len,那么next数组的定义应该有什么变化呢?
最直接的感觉是:p数组的下标整体+1,用第二种定义应该很简单地套用
我们定义next[i]为p[1,i]最长公共前后缀的长度,也是前缀最后一个元素的下标

void getNext(string p,int len){
      next[1]=0;//最长公共前后缀不能是字符串本身,p[1]的最长公前后缀长度为0,所以next赋值为0
      int j=0;//j的初值也要改
      for(int i=2;i<=len;i++){//求解next[2]~next[len]
          while(j!=0&&p[i]!=p[j+1]){
              j=next[j];
          }
          if(p[i]==p[j+1])j++;
          next[i]=j;
      }
  }

观察本题中next的定义,发现它有貌似结合了第一种定义与第二种定义,既表示最长公共前后缀的长度,也表示前缀最后一个元素的下标
如果从第二种定义(最长公共前后缀的长度)的角度来理解:原本第二种定义种,next[i]是p[0,i]最长公共前后缀的长度,前缀的区间为[0,next[i]-1],所以next[i]就是最后那个元素的下一个元素的下标.但是在现在这个问题中,由于p数组整体下标+1,所以第二种定义next[i]仍然表示p[1,i]最长公共前后缀的长度前缀的区间为[1,next[i]],所以next[i]是前缀最后一个元素的下标.也就是说,next第二种定义相比于第一种定义的"+1"的特性由于p也整体下标+1抵消了,所以当前字符匹配的条件仍然是p[i]==p[i+1],使得代码看起来和第一种定义很像.由于p地下标整体+1,所以要初始化next[1]而不是next[0]
如果从第一种定义的角度来看,next[0]原本初始化为-1,由于next[0]不用了,所以初始化next[1].如果next[1]初始化为-1,那么next[1]+1就不能正确地找到p[1],而是找到p[0].所以next[1]要初始化为0,同理j也要初始化为0;

絮絮叨叨

个人感觉第二种定义是最简洁地也是最好理解的,就采用字节擅长的定义去写就行,至少这样以后看到了其他同学写的kmp也能快速地把思想转换过来了.
作者还在学习中,如果本文某处有bug,欢迎指出.

  • 37
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值