next数组

  • 1. 如果对于值k,已有p0 p1, ..., pk-1 = pj-k pj-k+1, ..., pj-1,相当于next[j] = k
    • 此意味着什么呢?究其本质,next[j] = k 代表p[j] 之前的模式串子串中,有长度为k 的相同前缀和后缀。有了这个next 数组,在KMP匹配中,当模式串中j 处的字符失配时,下一步用next[j]处的字符继续跟文本串匹配,相当于模式串向右移动j - next[j] 位。

举个例子,如下图,根据模式串“ABCDABD”的next 数组可知失配位置的字符D对应的next 值为2,代表字符D前有长度为2的相同前缀和后缀(这个相同的前缀后缀即为“AB”),失配后,模式串需要向右移动j - next [j] = 6 - 2 =4位。

向右移动4位后,模式串中的字符C继续跟文本串匹配。

  • 2. 下面的问题是:已知next [0, ..., j],如何求出next [j + 1]呢?

对于P的前j+1个序列字符:

  • 若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1;
  • 若p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] =  next[k] + 1,否则继续递归前缀索引k = next[k],而后重复此过程。 相当于在字符p[j+1]之前不存在长度为k+1的前缀"p0 p1, …, pk-1 pk"跟后缀“pj-k pj-k+1, …, pj-1 pj"相等,那么是否可能存在另一个值t+1 < k+1,使得长度更小的前缀 “p0 p1, …, pt-1 pt” 等于长度更小的后缀 “pj-t pj-t+1, …, pj-1 pj” 呢?如果存在,那么这个t+1 便是next[ j+1]的值,此相当于利用已经求得的next 数组(next [0, ..., k, ..., j])进行P串前缀跟P串后缀的匹配。
   一般的文章或教材可能就此一笔带过,但大部分的初学者可能还是不能很好的理解上述求解next 数组的原理,故接下来,我再来着重说明下。
    如下图所示,假定给定模式串ABCDABCE,且已知next [j] = k(相当于“p0 pk-1” = “pj-k pj-1” = AB,可以看出k为2),现要求next [j + 1]等于多少?因为pk = pj = C,所以next[j + 1] = next[j] + 1 = k + 1(可以看出next[j + 1] = 3)。代表字符E前的模式串中,有长度k+1 的相同前缀后缀。

    但如果pk != pj 呢?说明“p0 pk-1 pk”  ≠ “pj-k pj-1 pj”。换言之,当pk != pj后,字符E前有多大长度的相同前缀后缀呢?很明显,因为C不同于D,所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:next[j + 1] = next[j] + 1 。所以,咱们只能去寻找长度更短一点的相同前缀后缀。

    结合上图来讲,若能在前缀“ p0 pk-1 pk ” 中不断的递归前缀索引k = next [k],找到一个字符pk’ 也为D,代表pk’ = pj,且满足p0 pk'-1 pk' = pj-k' pj-1 pj,则最大相同的前缀后缀长度为k' + 1,从而next [j + 1] = k’ + 1 = next [k' ] + 1。否则前缀中没有D,则代表没有相同的前缀后缀,next [j + 1] = 0。
    那为何递归前缀索引k = next[k],就能找到长度更短的相同前缀后缀呢?这又归根到next数组的含义。我们拿前缀 p0 pk-1 pk 去跟后缀pj-k pj-1 pj匹配,如果pk 跟pj 失配,下一步就是用p[next[k]] 去跟pj 继续匹配,如果p[ next[k] ]跟pj还是不匹配,则需要寻找长度更短的相同前缀后缀,即下一步用p[ next[ next[k] ] ]去跟pj匹配。此过程相当于模式串的自我匹配,所以不断的递归k = next[k],直到要么找到长度更短的相同前缀后缀,要么没有长度更短的相同前缀后缀。如下图所示:
    
    所以,因最终在前缀ABC中没有找到D,故E的next 值为0:
 
模式串的后缀:ABDE

模式串的前缀:ABC

前缀右移两位:     ABC

读到此,有的读者可能又有疑问了,那能否举一个能在前缀中找到字符D的例子呢?OK,咱们便来看一个能在前缀中找到字符D的例子,如下图所示:

    给定模式串DABCDABDE,我们很顺利的求得字符D之前的“DABCDAB”的各个子串的最长相同前缀后缀的长度分别为0 0 0 0 1 2 3,但当遍历到字符D,要求包括D在内的“DABCDABD”最长相同前缀后缀时,我们发现pj处的字符D跟pk处的字符C不一样,换言之,前缀DABC的最后一个字符C 跟后缀DABD的最后一个字符D不相同,所以不存在长度为4的相同前缀后缀。
    怎么办呢?既然没有长度为4的相同前缀后缀,咱们可以寻找长度短点的相同前缀后缀,最终,因在p0处发现也有个字符D,p0 = pj,所以p[j]对应的长度值为1,相当于E对应的next 值为1(即字符E之前的字符串“DABCDABD”中有长度为1的相同前缀和后缀)。
    综上,可以通过递推求得next 数组,代码如下所示:
  1. void GetNext(char* p,int next[])
  2. {
  3. int pLen = strlen(p);
  4. next[0] = -1;
  5. int k = -1;
  6. int j = 0;
  7. while (j < pLen - 1)
  8. {
  9. //p[k]表示前缀,p[j]表示后缀
  10. if (k == -1 || p[j] == p[k])
  11. {
  12. ++k;
  13. ++j;
  14. next[j] = k;
  15. }
  16. else
  17. {
  18. k = next[k];
  19. }
  20. }
  21. }

用代码重新计算下“ABCDABD”的next 数组,以验证之前通过“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next 数组是否正确,计算结果如下表格所示:

从上述表格可以看出,无论是之前通过“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next 数组,还是之后通过代码递推计算求得的next 数组,结果是完全一致的。

3.3.5 基于《next 数组》匹配

下面,我们来基于next 数组进行匹配。

还是给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:

在正式匹配之前,让我们来再次回顾下上文2.1节所述的KMP算法的匹配流程:

  • 假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
    • 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
    • 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。
      • 换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。
  • 1. 最开始匹配时
    • P[0]跟S[0]匹配失败
      • 所以执行“如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]”,所以j = -1,故转而执行“如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++”,得到i = 1,j = 0,即P[0]继续跟S[1]匹配。
    • P[0]跟S[1]又失配,j再次等于-1,i、j继续自增,从而P[0]跟S[2]匹配。
    • P[0]跟S[2]失配后,P[0]又跟S[3]匹配。
    • P[0]跟S[3]再失配,直到P[0]跟S[4]匹配成功,开始执行此条指令的后半段:“如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++”。

  • 2. P[1]跟S[5]匹配成功,P[2]跟S[6]也匹配成功, ...,直到当匹配到P[6]处的字符D时失配(即S[10] != P[6]),由于P[6]处的D对应的next 值为2,所以下一步用P[2]处的字符C继续跟S[10]匹配,相当于向右移动:j - next[j] = 6 - 2 =4 位。

  • 3. 向右移动4位后,P[2]处的C再次失配,由于C对应的next值为0,所以下一步用P[0]处的字符继续跟S[10]匹配,相当于向右移动:j - next[j] = 2 - 0 = 2 位。

  • 4. 移动两位之后,A 跟空格不匹配,模式串后移1 位。

  • 5. P[6]处的D再次失配,因为P[6]对应的next值为2,故下一步用P[2]继续跟文本串匹配,相当于模式串向右移动 j - next[j] = 6 - 2 = 4 位。

  • 6. 匹配成功,过程结束。

匹配过程一模一样。也从侧面佐证了,next 数组确实是只要将各个最大前缀后缀的公共元素的长度值右移一位,且把初值赋为-1 即可。

3.3.6 基于《最大长度表》与基于《next 数组》等价

我们已经知道,利用next 数组进行匹配失配时,模式串向右移动 j - next [ j ] 位,等价于已匹配字符数 - 失配字符的上一位字符所对应的最大长度值。原因是:

  1. j 从0开始计数,那么当数到失配字符时,j 的数值就是已匹配的字符数;
  2. 由于next 数组是由最大长度值表整体向右移动一位(且初值赋为-1)得到的,那么失配字符的上一位字符所对应的最大长度值,即为当前失配字符的next 值。

但为何本文不直接利用next 数组进行匹配呢?因为next 数组不好求,而一个字符串的前缀后缀的公共元素的最大长度值很容易求。例如若给定模式串“ababa”,要你快速口算出其next 数组,乍一看,每次求对应字符的next值时,还得把该字符排除之外,然后看该字符之前的字符串中有最大长度为多大的相同前缀后缀,此过程不够直接。而如果让你求其前缀后缀公共元素的最大长度,则很容易直接得出结果:0 0 1 2 3,如下表格所示:

然后这5个数字 全部整体右移一位,且初值赋为-1,即得到其next 数组:-1 0 0 1 2。

深入理解kmp中的next数组的更多相关文章

  1. 关于KMP中求next数组的思考【转】

    文章转自 http://www.tuicool.com/articles/yayeIbe.这是我看到关于求next数组,解释最好的一篇文章!!!!!!! KMP的next数组求法是很不容易搞清楚的一部 ...

  2. Seek the Name, Seek the Fame---poj2752(kmp中的Next数组)

    题目链接:http://poj.org/problem?id=2752 题意就是求出是已知s串的前缀的长度x,并且要求此前缀也是s串的后缀:求出所有的 x : Next[i]的含义是前i个元素的前缀和 ...

  3. POJ 2752 KMP中next数组的理解

    感觉这里讲的挺好的.http://cavenkaka.iteye.com/blog/1569062 就是不断递归next数组.长度不断减小. 题意:给你一个串,如果这个串存在一个长度为n的前缀串,和长 ...

  4. KMP中next数组的理解

    next数组是KMP的核心,但对于next数组我们总是有时候感觉明白了,但有时候又感觉没明白,现在我就说下我自己对KMP中next数组的理解,首先next[i]上的数字的意义,next[i]表示的是当 ...

  5. dutacm.club_1087_Common Substrings_(KMP)_(结合此题通俗理解kmp的next数组)

    1087: Common Substrings Time Limit:3000/1000 MS (Java/Others)   Memory Limit:163840/131072 KB (Java/ ...

  6. KMP 算法中的 next 数组

    KMP 算法中对 next 数组的理解 next 数组的意义 此处 next[j] = k:则有 k 前面的浅蓝色区域和 j 前面的浅蓝色区域相同: next[j] 表示当位置 j 的字符串与主串不匹 ...

  7. poj 2406:Power Strings(KMP算法,next[]数组的理解)

    Power Strings Time Limit: 3000MS   Memory Limit: 65536K Total Submissions: 30069   Accepted: 12553 D ...

  8. POJ 2752 KMP中next数组的应用

    题意: 让你从小到大输出给的字符串中既是前缀又是后缀的子串的长度. 思路: 先要了解这个东西: KMP中next数组表示的含义:记录着字符串匹配过程中失配情况下可以向前多跳几个字符,它描述的也是子串的 ...

  9. KMP算法中求next数组的实质

    在串匹配模式中,KMP算法较蛮力法是高效的算法,我觉得其中最重要的一点就是求next数组: 看了很多资料才弄明白求next数组是怎么求的,我发现我的忘性真的比记性大很多,每次看到KMP算法求next数 ...

随机推荐

  1. B树

    /************************************************ *作者:陈新 *时间:2014 6.3 *邮箱:cx2pirate@gmail.com * **** ...

  2. @autoreleasepool在MRC和ARC中的区别

    对于@autoreleasepool {} (1)在ARC中会销毁所有在里面创建的对象,即使你用外面的Strong指针指向他 (2)在MRC中如果有外部的强指针指向,不会销毁对象,retainCoun ...

  3. Sqrtx

    我只能想出二分的方法,而且还不一定能写出最简洁的代码.无论刷多少遍,牛顿迭代法我都想不到,莫名有种悲哀的感觉:智力是硬伤啊.就算如此,却还要一遍遍不厌其烦地刷,这才是最悲剧的.多说无益,上代码. 二分 ...

  4. php类的__get和__set方法

    (1)这两个方法是自动调用的 (2)这两个方法只有在成员变量是private的时候才会自己调用 testclass.php <?php class testclass { private $va ...

  5. Gnu/Linux的学习探索

    1.Gnu/Linux是一个基于POSIX和UNIX的多用户多任务 支持多线程多CPU的类UNIX的操作系统. 继承了UNIX以网络为核心的设计思想 是性能稳定的多用户网络操作系统. 1991年10月 ...

  6. 对vector<int>进行快速排序

    #include <iostream>#include <string>#include <vector>using namespace std;void Quic ...

  7. mysql 修改root密码

    1.找到配置文件my.ini  ,然后将其打开,可以选择用记事本打开 C:\Program Files (x86)\MySQL\MySQL Server 5.0 2.打开后,搜索mysqld关键字,找 ...

  8. 涵涵和爸爸习惯养成进度表(三)(June 25 - )

    规则说明 23天内,没有哭脸,不超过三个无表情脸,可以给一个奖励(动画书等) 涵涵违反规则,在爸爸和妈妈都同意的情况下,可以给无表情脸 爸爸违反规则,在妈妈和涵涵都同意的情况下,可以给无表情脸 获奖记 ...

  9. [SHELL] 修改xml的内容

    解析和修改xml用python比较方便,但如果不方便使用python,可以用sed命令简单替换 例如,欲替换下面一行中的端口号的值: <param name="ftpPort" ...

  10. C语言程序设计第7堂作业

         一.本次课主要内容: 本次以计算圆柱体体积为例,通过定义体积计算功能的函数和主函数调用的例子,引出函数定义的一般形式:函数首部加函数体,且在函数结尾处通过return 语句返回结果.本节要重 ...