深入理解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繼續跟文本串匹配。

  • 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 數組,代碼如下所示:
[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 數組是否正確,計算結果如下表格所示:

    從上述表格可以看出,無論是之前通過“最長相同前綴后綴長度值右移一位,然后初值賦為-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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM