KMP匹配算法詳解

     假如,A="abababaababacb",B="ababacb",我們來看看KMP是怎麼工作的。我們用兩個指針i和j分別表示,A[i-j+ 1..i]與B[1..j]完全相等。也就是說,i是不斷增加的,隨着i的增加j相應地變化,且j滿足以A[i]結尾的長度爲j的字符串正好匹配B串的前 j個字符(j當然越大越好),現在需要檢驗A[i+1]和B[j+1]的關係。當A[i+1]=B[j+1]時,i和j各加一;什麼時候j=m了,我們就說B是A的子串(B串已經整完了),並且可以根據這時的i值算出匹配的位置。當A[i+1]<>B[j+1],KMP的策略是調整j的位置(減小j值)使得A[i-j+1..i]與B[1..j]保持匹配且新的B[j+1]恰好與A[i+1]匹配(從而使得i和j能繼續增加)。我們看一看當 i=j=5時的情況。

     i  = 1 2 3 4 5 6 7 8 9 ……
     A = a b a b a b a a b a b …
     B = a b a b a c b
     j  = 1 2 3 4 5 6 7

     此時,A[6]<>B[6]。這表明,此時j不能等於5了,我們要把j改成比它小的值j'。j'可能是多少呢?仔細想一下,我們發現,j'必須要使得B[1..j]中的頭j'個字母和末j'個字母完全相等(這樣j變成了j'後才能繼續保持i和j的性質)。這個j'當然要越大越好。在這裏,B [1..5]="ababa",頭3個字母和末3個字母都是"aba"。而當新的j爲3時,A[6]恰好和B[4]相等。於是,i變成了6,而j則變成了 4:

     i  = 1 2 3 4 5 6 7 8 9 ……
     A = a b a b a b a a b a b …
     B =       a b a b a c b
     j  =       1 2 3 4 5 6 7

     從上面的這個例子,我們可以看到,新的j可以取多少與i無關,只與B串有關。我們完全可以預處理出這樣一個數組P[j],表示當匹配到B數組的第j個字母而第j+1個字母不能匹配了時,新的j最大是多少。P[j]應該是所有滿足B[1..P[j]]=B[j-P[j]+1..j]的最大值。
     再後來,A[7]=B[5],i和j又各增加1。這時,又出現了A[i+1]<>B[j+1]的情況:

     i  = 1 2 3 4 5 6 7 8 9 ……
     A = a b a b a b a a b a b …
     B =       a b a b a c b
     j  =       1 2 3 4 5 6 7

     由於P[5]=3,因此新的j=3:

     i  = 1 2 3 4 5 6 7 8 9 ……
     A = a b a b a b a a b a b …
     B =             a b a b a c b
     j  =             1 2 3 4 5 6 7

     這時,新的j=3仍然不能滿足A[i+1]=B[j+1],此時我們再次減小j值,將j再次更新爲P[3]:

     i  = 1 2 3 4 5 6 7 8 9 ……
     A = a b a b a b a a b a b …
     B =             a b a b a c b
     j  =             1 2 3 4 5 6 7

     現在,i還是7,j已經變成1了。而此時A[8]居然仍然不等於B[j+1]。這樣,j必須減小到P[1],即0:

     i = 1 2 3 4 5 6 7 8 9 ……
     A = a b a b a b a a b a b …
     B =                   a b a b a c b
     j =                    0 1 2 3 4 5 6 7

     終於,A[8]=B[1],i變爲8,j爲1。事實上,有可能j到了0仍然不能滿足A[i+1]=B[j+1](比如A[8]="d"時)。因此,準確的說法是,當j=0了時,我們增加i值但忽略j直到出現A[i]=B[1]爲止。
     這個過程的代碼很短(真的很短),我們在這裏給出:

j:=0;
for i:=1 to n do
begin
 
  while (j>0) and (B[j+1]<>A[i]) do j:=P[j];
   if B[j+1]=A[i] then j:=j+1;
   if j=m then
   begin
       writeln('Pattern occurs with shift ',i-m);
       j:=P[j];
   end;
end;


     最後的j:=P[j]是爲了讓程序繼續做下去,因爲我們有可能找到多處匹配。
     這個程序或許比想像中的要簡單,因爲對於i值的不斷增加,代碼用的是for循環。因此,這個代碼可以這樣形象地理解:掃描字符串A,並更新可以匹配到B的什麼位置。

     現在,我們還遺留了兩個重要的問題:一,爲什麼這個程序是線性的;二,如何快速預處理P數組。
     爲什麼這個程序是O(n)的?其實,主要的爭議在於,while循環使得執行次數出現了不確定因素。我們將用到時間複雜度的攤還分析中的主要策略,簡單地說就是通過觀察某一個變量或函數值的變化來對零散的、雜亂的、不規則的執行次數進行累計。KMP的時間複雜度分析可謂攤還分析的典型。我們從上述程序的j 值入手。每一次執行while循環都會使j減小(但不能減成負的),而另外的改變j值的地方只有第五行。每次執行了這一行,j都只能加1;因此,整個過程中j最多加了n個1。於是,j最多隻有n次減小的機會(j值減小的次數當然不能超過n,因爲j永遠是非負整數)。這告訴我們,while循環總共最多執行了n次。按照攤還分析的說法,平攤到每次for循環中後,一次for循環的複雜度爲O(1)。整個過程顯然是O(n)的。這樣的分析對於後面P數組預處理的過程同樣有效,同樣可以得到預處理過程的複雜度爲O(m)。
     預處理不需要按照P的定義寫成O(m^2)甚至O(m^3)的。我們可以通過P[1],P[2],...,P[j-1]的值來獲得P[j]的值。對於剛纔的B="ababacb",假如我們已經求出了P[1],P[2],P[3]和P[4],看看我們應該怎麼求出P[5]和P[6]。P[4]=2,那麼P [5]顯然等於P[4]+1,因爲由P[4]可以知道,B[1,2]已經和B[3,4]相等了,現在又有B[3]=B[5],所以P[5]可以由P[4] 後面加一個字符得到。P[6]也等於P[5]+1嗎?顯然不是,因爲B[ P[5]+1 ]<>B[6]。那麼,我們要考慮“退一步”了。我們考慮P[6]是否有可能由P[5]的情況所包含的子串得到,即是否P[6]=P[ P[5] ]+1。這裏想不通的話可以仔細看一下:

           1 2 3 4 5 6 7
     B = a b a b a c b
     P = 0 0 1 2 3 ?

     P[5]=3是因爲B[1..3]和B[3..5]都是"aba";而P[3]=1則告訴我們,B[1]、B[3]和B[5]都是"a"。既然P[6]不能由P[5]得到,或許可以由P[3]得到(如果B[2]恰好和B[6]相等的話,P[6]就等於P[3]+1了)。顯然,P[6]也不能通過P[3]得到,因爲B[2]<>B[6]。事實上,這樣一直推到P[1]也不行,最後,我們得到,P[6]=0。
     怎麼這個預處理過程跟前面的KMP主程序這麼像呢?其實,KMP的預處理本身就是一個B串“自我匹配”的過程。它的代碼和上面的代碼神似:

P[1]:=0;
j:=0;
for i:=2 to m do
begin
 
  while (j>0) and (B[j+1]<>B[i]) do j:=P[j];
   if B[j+1]=B[i] then j:=j+1;
   P[i]:=j;
end;


     最後補充一點:由於KMP算法只預處理B串,因此這種算法很適合這樣的問題:給定一個B串和一羣不同的A串,問B是哪些A串的子串。

     串匹配是一個很有研究價值的問題。事實上,我們還有後綴樹,自動機等很多方法,這些算法都巧妙地運用了預處理,從而可以在線性的時間裏解決字符串的匹配。

 

 

最值得注意的是kmp算法的next數組的含義:若在第i+1個位置,next的值爲k,則i-k表示下次匹配模式串要先前挪動的字符個數,設挪動的個數爲3,則前i個字符必爲a1a2a3a1a2a3a1a2a3a1....的形式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章