KMP之next数组从何而来

KMP之next数组从何而来

引言:

本人在学习kmp算法时,对next数组的形成一直云里雾里,看了一些文章觉得还可以解释更细,然后看到一篇很详尽透彻的文章KMP算法详解-彻底清楚了(转载+部分原创) - sofu6 - 博客园 (cnblogs.com),本文将写下对上面文章中next数组推导部分的自我理解补充

注:本文只关注next数组的推导,而非整个kmp算法,解释会尽量详细,如遇已懂部分,请自行跳读

预备知识

  • 前缀、后缀、串

一般算法

代码

public static int[] getNext(String ps) 
{
    char[] p = ps.toCharArray();
    int[] next = new int[p.length];
    next[0] = -1;
    int j = 0;
    int k = -1;
    while (j < p.length - 1) {
       if (k == -1 || p[j] == p[k]) {
           next[++j] = ++k;
       } else {
           k = next[k];
       }
    }
    return next;
}

解析

方法:递推
变量解释:
  • j:一个普通的下标
  • p:串的名称
  • k:j下标元素之前的元素形成的子串中(不含p[ j ]),最长的相同前后缀的前(后)缀元素个数,也叫next[ j ]
    例如:abad中,对于j = 3 (对应p[ 3 ]为d),j元素之前元素形成的子串为aba,所有前缀组成的集合为{‘a’, ‘ab’},所有后缀组成的集合为{‘a’, ‘ba’},则最长相同前后缀为’a’,元素个数为1,则j = 3对应的k为1,也称next[ j ] 为1。
  • pi:就是p[ i ],鄙人懒蛋一个,就不加中括号啦
  • ⑤:圈某代表某条条件
注意:

本文解释令串从下标0开始(即串的第一个元素下标是0),请注意区分下标x、第x个、长度为x

解释:

先找出递推公式

条件:类似数学归纳法,我们假设刚刚得到某下标 j 时的next[ j ](得到 j 时对应的k),这说明:j下标元素之前的元素形成的子串中(不含p[ j ]),最长的相同前后缀的前(后)缀元素个数为k (实用的复制粘贴)。即p0,p1,……,pk-1,形成的子串与pj-k,pj-k+1,……,pj-1形成的子串相同

加油

现在要求next[ j + 1 ],分两种情况

  • 情况1:pk == pj(对应第9行if的第二个相等判断)

    先捏软柿子,这情况简单,两个元素相同时,则可以把前k个元素形成的子串,往后延长一个长度,此时next[ j + 1]为k + 1,然后再使j自增,k自增找下一个元素的next数(对应第10行赋值与自增)

    这说明:j + 1下标元素之前的元素形成的子串中(不含p[ j + 1 ]),最长的相同前后缀的前(后)缀元素个数为k + 1 (又是实用的复制粘贴)。即 p0,p1,……,pk-1,pk,形成的子串与pj-k,pj-k+1,……,pj-1,pj形成的子串相同

    加油加油

  • 情况2:pk != pj

    重头戏来了,当这两个元素不相同时,前后缀最大重复子串长度<k + 1,(证:如果长度等于k+1,则成了第一种情况,但显然这是第二种情况么,如果大于k+1,则说明pj前有大于长度k的前后缀重复子串,与前提不符),
    然后让长度递减地找有没有最大前后缀重复子串,长度递减停止在第一次(之所以是第一次是为了保证此前后缀重复子串最长)出现i使得p0,p1,……,pi-1,pi子串相同于pj-i,pj-i+1,……,pj-1,pj子串

    加油马上了

    现在来求满足上述条件的i,我们把要满足①分成要同时满足②③两条件

    • p0,p1,……,pi-1子串相同于pj-i,pj-i+1,……,pj-1子串

      如果要满足②,由初始条件(已得到某下标 j 时的next[ j ])得p0,p1,……,pk-1子串相同于pj-k,pj-k+1,……,pj-1子串,则pk-i,pk-i+1,……,pk-1子串相同于pj-i,pj-i+1,……,pj-1子串④

      ④条件下,②条件与,p0,p1,……,pi-1子串相同于pk-i,pk-i+1,……,pk-1子串这个条件是充要关系,求满足②转化为求满足⑤,最后啦

      注意到⑤的两个子串是p0~pk-1的前后缀,求p0 ~ pk-1(pk之前)的最大前后缀子串就是求next[ k ],而k一定小于j,而next[ j ]都求出来了,next[k]肯定更早就知道了,则i-1最大是next[ k ] - 1(0~next[k-1]一共是next[k]个元素),i为next[k],此时⑤满足,②也随之满足(对应第12行赋值,代码里使用k被赋值,这里为了区分开讲的更清楚用了i)

    • pi相同于pj

      ②条件先满足了再看③,③是否满足分为pi == pj和pi != pj两种情况,此时的i就相当于初始时的k(这里的相当于指的不是大小相等,而是接下来对有关i执行的操作和一开始(不是最最开始,而是④,即我递推的开始)对有关k执行的操作相同),则相等就进入递推的情况1(赋值、自增),不等就进入情况2(赋值、判断)(对应第9行if第二个相等判断),当i(或k,叫啥都行)为-1时,说明已经进行过i等于0的操作(因为next[0]是-1,i要想成为-1,得执行i=next[0],则i得先是0),然后p0都不等于pj,则子串长度再缩短为0了,pj+1前最大前后缀重复子串长度为0,赋next[j+1] = 0,j自增找下一个的next值,k自增(对应第9行if第一个判断、第10行赋值、两个自增)

      其实next[0]只要不是自然数就行,比如自定义为-6,则代码可写成

      if (k == -6)
      {
          next[++j] = 0;
          k = 0;//之所以k要赋值0,是因为非首元素的k有含义,即最大前后缀重复子串,这里代表j+1下标元素最大前后缀重复子串长度为0
      }
      if (p[j] == p[k])
      	next[++j] = ++k;
      else
      	k = next[k];
      

      不过显然没有next[0]赋值为-1代码更简洁

找到了递推公式,再看看一开始取什么,由上面知道next[0]=-1使代码更简洁,接着next[0]中的0就是j的值,next[0]就是k的值,则k=-1,j=0,其实next[1]也能知道是0,但主串不一定长度超过1,所以只赋next[0]

总结:

感谢阅读,本文之后预计有时间补充优化算法的解析,如果对本文是我对kmp中next诞生的理解,如果有任何疑问或者发现解释问题,请私信或在评论区里留言

希望本文能帮助到大家,这是我第二次写文章,还有很多用语、严谨性、生动性等待锤炼,我会朝着写出更生动易懂实用的文章不断进发,再次感谢阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值