深刻理解kmp中的next数组

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继续跟文本串匹配。spa

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

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

  • 若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 数组,代码以下所示:
[cpp]  view plain  copy
 
 print?在CODE上查看代码片派生到个人代码片
  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 数组是否正确,计算结果以下表格所示:code

    从上述表格能够看出,不管是以前经过“最长相同前缀后缀长度值右移一位,而后初值赋为-1”获得的next 数组,仍是以后经过代码递推计算求得的next 数组,结果是彻底一致的。blog

3.3.5 基于《next 数组》匹配

    下面,咱们来基于next 数组进行匹配。递归

 

    仍是给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,如今要拿模式串去跟文本串匹配,以下图所示:索引

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

  • 假设如今文本串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。

相关文章
相关标签/搜索