【KMP】从原理上详解next数组和nextval数组

本文将从原理上详细解释KMP算法中的next数组以及nextval数组,尽量让大家明白它们到底在记录什么,为什么要这样算。以及现在普遍的KMP算法实现当中的next数组与前两者有何不同。篇幅较长,但尽量讲清楚。

next数组

next数组到底在记录什么?

虽然数据结构中对next数组有定义,但并不易于理解,因此我个人对next数组进行了一个简单解释:

next数组指示了当前模式串在该位置匹配冲突(即失配,个人习惯称冲突)时,应该将模式串的哪一位与此位对齐。

这里为刚学习KMP的朋友解释一下,KMP算法进行字符串匹配时,被检索的串称为“文本串”,要检索的目标串称为“模式串”。next数组与nextval数组的求解只与模式串有关。

举个例子
假设需要在字符串 “abaabbcabaabcac” 中查找是否存在字符串 “abaabcac”,那么 "abaabbcabaabcac"就是文本串,"abaabcac"就是模式串。

这里其它的许多详解中会直接告诉你,next数组的第一位是0,第二位是1,然后从第三位开始继续算什么的,但很少看到有解释为什么第一位一定是0,第二位一定是1

为了解释这个问题,我们首先要搞清楚模式串的“位”是什么样的。
对“abaabcac”来说,第一个字符a就是第一位,不要受一些编程语言思维的影响认为这是第0位。

位数12345678
模式串abaabcac

但是为了便于理解,我们假想模式串的最前面还有一个空白字符位,称为第0位,即:

位数012345678
模式串abaabcac

现在,我们再来对照前面给出的定义:
next数组指示了当前模式串在该位置匹配冲突时,应该将模式串的哪一位与此位对齐。

是不是就理解为什么第一位一定是0,第二位一定是1了?
还没有?那我们再假想一个文本串加上来。

待求位:      1  2  3  4  5  6  7  8
文本串:      x  x  x  x  x  x  x  x  x  x  x  x  x  x  x  x
模式串:      a  b  a  a  b  c  a  c

现在标红的位置发生冲突,那么下一步应该让模式串的哪一位跟当前冲突的 x 位对齐呢?没错,就是第0位。也就是变成:

待求位:      1  2  3  4  5  6  7  8
文本串:      x  x  x  x  x  x  x  x  x  x  x  x  x  x  x  x
模式串:          a  b  a  a  b  c  a  c
位数:          0  1  2  3  4  5  6  7  8

所以,当模式串的第一位发生冲突时,应该将模式串的第0位与当前位置对齐,也就是next[1]=0;


继续看第二位发生冲突的情况:

待求位:      1  2  3  4  5  6  7  8
文本串:      a  x  x  x  x  x  x  x  x  x  x  x  x  x  x  x
模式串:      a  b  a  a  b  c  a  c

此时需要将模式串移动为:

待求位:      1  2  3  4  5  6  7  8
文本串:      a  x  x  x  x  x  x  x  x  x  x  x  x  x  x  x
模式串:          a  b  a  a  b  c  a  c
位数:          0  1  2  3  4  5  6  7  8

所以,当模式串的第二位发生冲突时,应该将模式串的第1位与当前位置对齐,也就是next[2]=1;

无论模式串如何变化,只要其长度大于等于2,这两种情况都是固定的,因此next[1]一定为0,next[2]一定为1。


继续看第三位发生冲突的情况:

待求位:      1  2  3  4  5  6  7  8
文本串:      a  b  x  x  x  x  x  x  x  x  x  x  x  x  x  x
模式串:      a  b  a  a  b  c  a  c

这时,假如我们仍然只移动一位,可以吗?

待求位:      1  2  3  4  5  6  7  8
文本串:      a  b  x  x  x  x  x  x  x  x  x  x  x  x  x  x
模式串:          a  b  a  a  b  c  a  c

可以发现,如果仍然只移动一位,那么 x 位之前的位置也出现了冲突!

因此究竟该将模式串的哪一位与冲突的位置对齐呢?
一个重要原则是:当前冲突位置之前的一位不能有冲突!

换句话说,要解决第三位的冲突,移动模式串时起码要保证第二位是无冲突的。

那现在出现冲突了怎么办呢?



想想next数组本身的作用?不就是指示了出现了冲突时,应该将哪一位与该位置对齐吗?!

想到这里,问题就变得简单了。

现在第二位出现了冲突,按next[2]指示应该将第一位的a与该位置对齐,那现在冲突解决了吗?
显然b不等于a,没有解决。但是问题已经转化成了第一位的位置出现冲突了!
继续查next[1],等于0,也就是说需要将模式串的第0位与该位置对齐。第0位是空的,我们认为它一定不会产生冲突,至此,第二位的冲突顺利解决了,那现在我们和第二位对齐的是哪一位呢?没错,是第0位,如下:

待求位:      1  2  3  4  5  6  7  8
文本串:      a  b  x  x  x  x  x  x  x  x  x  x  x  x  x  x
模式串:              a  b  a  a  b  c  a  c
位数:              0  1  2  3  4  5  6  7  8

那和第三位(待解决的冲突位)对齐的自然是第0位的下一位,即第一位。由此,便可以得到next[3]=1

而这个过程就是其他计算详解中所提到的,【找到当前要求的next字符的前一个字符,以它为标准,找到其next对应下标的字符,和这个字符做比较。若相等,那么当前字符的next值就位此字符next加一;若不等,继续找现在字符next所指的下一个字符,还是和之前的字符比较,直到找到第一个位置为止,那么next为1。】其实就是依据现有next值解决前一位冲突的过程。


为了让大家更熟悉这个过程,再继续看一下第四位发生冲突的情况:

待求位:      1  2  3  4  5  6  7  8
文本串:      a  b  a  x  x  x  x  x  x  x  x  x  x  x  x  x
模式串:      a  b  a  a  b  c  a  c

按前面所讲,要确定需要让哪一位与第四位对齐,起码要保证第三位是无冲突的。
第三位是什么?字符a,第三位冲突时需要对齐的是哪一位?是next[3]=1,即第一位的字符a。
显然a和a是相等的,至此第三位的冲突已经解决,我们将模式串的第一位与第三位对齐,如下:

待求位:      1  2  3  4  5  6  7  8
文本串:      a  b  a  x  x  x  x  x  x  x  x  x  x  x  x  x
模式串:              a  b  a  a  b  c  a  c
位数:              0  1  2  3  4  5  6  7  8

那和第四位(待解决的冲突位)对齐的自然是第一位的下一位,即第二位。由此,便可以得到next[4]=2


第五位冲突的情况:

待求位:      1  2  3  4  5  6  7  8
文本串:      a  b  a  a  x  x  x  x  x  x  x  x  x  x  x  x
模式串:      a  b  a  a  b  c  a  c

至少保证第四位无冲突,第四位为字符a,next[4]=2,所指示字符为第二位的b,a不等于b,转化为第二位的冲突;next[2]=1,所指示的字符位第一位的a,a等于a,冲突解决。此时和第四位对齐的是第一位的a,如下:

待求位:      1  2  3  4  5  6  7  8
文本串:      a  b  a  a  x  x  x  x  x  x  x  x  x  x  x  x
模式串:                  a  b  a  a  b  c  a  c
位数:                  0  1  2  3  4  5  6  7  8

那和第五位(待解决的冲突位)对齐的自然是第一位的下一位,即第二位。由此,便可以得到next[5]=2


第六位冲突的情况:

待求位:      1  2  3  4  5  6  7  8
文本串:      a  b  a  a  b  x  x  x  x  x  x  x  x  x  x  x
模式串:      a  b  a  a  b  c  a  c

至少保证第五位无冲突,第五位为字符b,next[5]=2,所指示字符为第二位的b,b等于b,冲突解决。此时和第五位对齐的是第二位的b,如下:

待求位:      1  2  3  4  5  6  7  8
文本串:      a  b  a  a  b  x  x  x  x  x  x  x  x  x  x  x
模式串:                  a  b  a  a  b  c  a  c
位数:                  0  1  2  3  4  5  6  7  8

那和第六位(待解决的冲突位)对齐的自然是第二位的下一位,即第三位。由此,便可以得到next[6]=3


第七位和第八位的冲突大家可以自行计算,最终便可以得到模式串的next数组值:

位数12345678
模式串abaabcac
next值01122312

nextval数组

nextval数组在记录什么? 为什么需要nextval数组?

其实和next数组一样,nextval数组也是用来指示当前模式串在该位置匹配冲突时,应该将模式串的哪一位与此位对齐的

既然nextval数组的作用和next数组一样,那为什么需要nextval数组呢?
在严蔚敏的《数据结构(C语言版)》中,说明了next数组存在的问题,我们同样以该例子来讲。

假设文本串为 “aaabaaaab”,模式串为 “aaaab”。
根据前面的讲述,我们可以得到模式串的next数组值如下:

位数12345
模式串aaaab
next值01234

冲突发生在第四位:

文本串:      a  a  a  b  a  a  a  a  b
模式串:      a  a  a  a  b

根据next值,next[4]=3,需要将第3位与该位置对齐:

文本串:      a  a  a  b  a  a  a  a  b
模式串:          a  a  a  a  b

但此时仍有冲突,这时冲突发生在模式串的第三位,根据next[3]=2,需要将第二位与此位置对齐:

文本串:      a  a  a  b  a  a  a  a  b
模式串:              a  a  a  a  b

仍有冲突,发生在模式串第二位,next[2]=1,将模式串第一位与此位置对齐:

文本串:      a  a  a  b  a  a  a  a  b
模式串:                  a  a  a  a  b

依然冲突,发生在模式串第一位,next[1]=0,将模式串第0位与此位置对齐:

文本串:      a  a  a  b  a  a  a  a  b
模式串:                      a  a  a  a  b

至此,冲突解决,匹配成功。
但整个过程效率很低,因为模式串前四个字符都为a,当第一次发生冲突时,就可以知道应该直接将第0位和该位置对齐,中间的几次比较和移动都是多余的。
因此,nextval所做的工作就是对next值进行一次修正,排除掉无效移动。

而计算nextval值也有一个原则:保留不同

为什么说是保留不同呢?分析一下上述例子问题产生的原因就清楚了:

由于计算next值时,我们不清楚当前位置的情况,最多只考虑到了冲突的前一位,就可能导致移动后对齐的字符仍和上一个冲突的字符相同,而这种情况必然会继续冲突。

所以我们要对next值进行一次筛选,保证冲突位置指示的应该对齐的字符和原字符不同。(注:这里我们认为第0位的空白字符与所有字符都不同)

以aaaab为例:
next[1]=0,第一位冲突指示的第0位为空白字符,和第一位本身的a不同,保留原next值,故nextval[1]=0;
next[2]=1,第二位冲突指示的第一位为字符a,和第二位本身的a相同,继续看第一位next[1]的值,(因为相同就表示按next值的此次对齐是无效的,会继续发生第一位的冲突,因此继续看next[1])next[1]=0,指示的第0位位空白字符,和第二位本身的a不同,保留next[1]的值,故nextval[2]=0;
以此类推,可以得到nextval值如下:

位数12345
模式串aaaab
next值01234
nextval值00004

现在普遍的KMP实现算法中的next数组又在记录什么?

关于这个问题,似乎很少有人进行解释,但这个问题却很容易让人困惑。因为现在普遍的KMP实现算法中所谓的next数组记录的东西和前面所说的next数组以及nextval数组是有区别的

为了进行区分,我们称这些KMP实现算法中的next数组为Knext数组

虽然Knext数组在使用时的功能和next数组以及nextval数组相似,但是其记录的内容和求解方法却与next以及nextval有着很大不同。

由于nextval与next没有本质区别,我们单比较Knext与next之间的异同。

相同之处

  • Knext和next都是为了提高算法效率,让串的匹配快速找到下一个可能的起始位置,避免文本串指针的回退。
  • Knext和next的求解过程都属于动态规划,需要依赖于之前求解出的Knext值和next值。

不同之处

  • Knext记录的是到对应位为止的最长相等前后缀的长度

    这一点可以说是Knext与next最本质的区别,next直接指示了冲突发生时需要对齐的模式串位置,而Knext则是提供了一个基于前缀后缀的参考,若要直接将其作为指示当前位置冲突时需要对齐的模式串位置(该对齐仍然是基于最长相等前后缀进行选择的,但next和nextval不会涉及前后缀的概念),需要做另外的处理(如统一减1或整体右移)。

    这里补充一下什么是最长相等前后缀
    前缀:不包含最后一个字符的所有以第一个字符开头的连续子串
    后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串

    以模式串 aaabbab 为例,
    第一位a:没有前缀和后缀,因此最长相等前后缀长度为0,即Knext[1]=0;
    第二位a:前缀 {a},后缀 {a},最长相等前后缀长度为1,即Knext[2]=1;
    第三位a:前缀 {a,aa},后缀{a,aa},最长相等前后缀长度为2,即Knext[3]=2;
    第四位b:前缀{a,aa,aaa},后缀{b,ab,aab},最长相等前后缀长度为0,即Knext[4]=0;
    第五位b:前缀{a,aa,aaa,aaab},后缀{b,bb,abb,aabb},最长相等前后缀长度为0,即Knext[5]=0;
    第六位a:前缀{a,aa,aaa,aaab,aaabb},后缀{a,ba,bba,abba,aabba},最长相等前后缀长度为1,即Knext[6]=1;
    第七位b:前缀{a,aa,aaa,aaab,aaabb,aaabba},后缀{b,ab,bab,bbab,abbab,aabbab},最长相等前后缀长度为0,即Knext[7]=0。

    为了更直观地看出它们之间的差异,这里给出模式串aaabbab的next、nextval和Knext值:
位数1234567
模式串aaabbab
next值0123112
nextval值0003102
Knext值0120010

关于Knext的求解方法这里不再赘述,可以参考代码随想录的相关讲解。

下方代码块是一个典型的Knext数组求解的实现。

void getNext(int* next, const string& s) {
        int j = 0;
        next[0] = 0;
        for(int i = 1; i < s.size(); i++) {
            while (j > 0 && s[i] != s[j]) { 
                j = next[j - 1]; 
            }
            if (s[i] == s[j]) {
                j++;
            }
            next[i] = j;
        }
    }

根据个人习惯不同,Knext的求解代码可能会在初始化和比较判断时有差异,但其本质是相同的。

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值