(閱讀本文前須要瞭解KMP算法的基本思路。另外,本着大道至簡的思想,本文的全部例子都會作從頭至尾的講解)java
做者翻閱了大量網上現有的KMP算法博客,發現廣爲流傳的居然是一種不完整的KMP算法。即經過next數組來做爲有限狀態自動機,以此實現非匹配時的回退。這不失爲一種好的方法。c++
但咱們接下來要見識的是一種更好和更完整的方法————擁有完整DFA的KMP算法算法
先列出本文要介紹的方法與通常方法對比下的幾大優勢:數組
(讀者能夠在通讀全文以後回頭來看這幾句話到底對不對)性能
kmp算法模擬了有限狀態自動機的運行,通常算法中的next數組和本文中的dfa數組都是做爲有限狀態自動機的運行指導。學習
有限狀態自動機不一樣,程序運行起來天然會存在不一樣。測試
在本文介紹的KMP算法中,咱們使用二維數組DFA來做爲有限狀態自動機指導:this
圖1 和模式字符串ABABAC對應的肯定有限狀態機自動機 spa
圖一展現了模式字符串pat:ABABAC對應的肯定有限狀態機自動機3d
dfa[A][j]表示:模式串成功匹配到第j個位置時文本這時對應字符爲'A'的狀況下模式串下一個將要匹配的位置。
拿圖1來講,dfa[A][3]表示匹配到模式串ABABAC的第三位時(B),文本對應的是A,這時模式串將回到dfa[A][3]=1,也就是將模式串回到ABABAC的第一位(B),而後繼續下一位(也是就ABABAC中的第二位,這裏是A)與文本的下一位繼續比較。
彷佛蠻複雜的,但理解了它的構造方法之,你就能夠靈活使用它。
咱們須要藉助j和X來構造dfa,j指向當前的匹配位置,X是匹配失敗時的重啓位置。一開始j和X都設爲0。
對於每一個j,咱們要作的是:
用代碼表示以下:
(推薦讀者先大概看看代碼,再結合下面給出的完整例子,而後作代碼運行調試)
dfa[pat.charAt(0)][0]=1; for(int X=0,j=1;j<M;j++){//計算dfa[][j]
for(int c=0;c<R;c++){//不匹配狀況
dfa[c][j]=dfa[c][X]; } dfa[pat.charAt(j)][j]=j+1; X=dfa[pat.charAt(j)][X]; }
在上面代碼的基礎上來演示一個完整的構造過程:
① j和X都爲0,dfa[pat.charAt(0)][0]=1
② 進入for循環X=0,j=1:將X的列複製到j的列,再設dfa[pat.charAt(j)][j]=j+1,更新X
能夠看到第三步更新X後X仍是0,由於在第二步時X=dfa[pat.charAt(j)][X]=dfa[B][0]=0 (關於X變化的探討接下來就會提到)
③ 第二次循環X=0,j=2:將X的列複製到j的列,再設dfa[pat.charAt(j)][j]=j+1,更新X
X=dfa[pat.charAt(j)][X]=dfa[A][0]=1
④ 第三次循環X=1,j=3:將X的列複製到j的列,再設dfa[pat.charAt(j)][j]=j+1,更新X
X=dfa[pat.charAt(j)][X]=dfa[B][1]=2
⑤ 第四次循環X=2,j=4:將X的列複製到j的列,再設dfa[pat.charAt(j)][j]=j+1,更新X
X=dfa[pat.charAt(j)][X]=dfa[A][2]=3
⑥ 第四次循環X=3,j=5:將X的列複製到j的列,再設dfa[pat.charAt(j)][j]=j+1,已經結束到最後一位,不用更新X
到這裏就結束了模式字符串ABABAC的dfa構造最終獲得的結果:
相信你們已經明白了dfa的構造思路
爲鞏固練習,下面請讀者本身構造出模式字符串ABRACAD的daf,而後和下圖對照一下是否是同樣
二、關於X的一些問答:
值得一提的是,X是構造dfa的關鍵,下面幾個問答有助於咱們理解整個dfa構造。
爲何每次都能得出X的值?
答:由於X永遠小於j,X走的是j走的老路。
爲何要把X列複製到j列?
答:dfa裏記錄了到每種狀態時可能的全部選擇,若是狀態A發生不匹配時能夠回到狀態B繼續匹配,那咱們就能夠先把狀態B複製到狀態A,這樣在狀態A不匹配時就能夠直接使用狀態B的方案。
X的位置什麼時候會發生變化?
X的下一個位置與j當前指向的字符、j以前指向過的字符、X當前位置都有關,事實上無論j當前指向的字符在以前是否出現過,X均可能移動。
X的位置會怎麼變化?
當每次j指向的字符與X指向的字符可以連續對應上的時候,X就會每次向後移一位(字符與前綴對應時X日後移)。
當j指向的字符在以前沒有出現過,X就會指向0。
三、實例對問題的證實:
上圖是模式ABCDE的dfa數組,能夠觀察到ABCDE中是沒有出現重複字符的,因此到最後X依然指向0
對應極端狀況,前面的字符出現重複達到了四次,X也是要移動四次,但只停留在3是由於模式串已經匹配完成,不須要再移動X。
關於X的移動,是須要讀者本身在模擬dfa構造中細想的,想明白了就能全懂KMP,不明白就再看看上面的問題,嘗試本身做答就會有新的心得。
有了強大的有限狀態自動機,怎麼用它呢?實際使用中是否比原來更強大呢?咱直接將二者的代碼貼出來一頓對比,順便說明精妙之處。
大致的思路是同樣的,就是將txt字符串從頭至尾循環一遍,過程當中不斷判斷模式串的位置
一、先來看看通常方法中的搜索方法代碼:
for(i=0;i<n;i++){ while (j>-1&&txt.charAt(i)!=pat.charAt(j)){ j=next[j]; } if(j==-1||txt.charAt(i)==pat.charAt(j)){ j++; } if(j==m){return i-j; } }
一邊從頭至尾循環,一邊判斷j是否是等於m,應該注意到的是,for循環中還包含了一個while,用來作回退和繼續匹配的。
能夠發現,這個過程當中的操做次數一定是要大於i的(每次for循環均可能要加入while)
二、下面是使用dfa後的搜索方法:
for(j=0,i=0;i<N&&j<M;i++){ j=dfa[txt.charAt(i)][j]; } if(j==M){ System.out.println("匹配成功"); return i-M; }else { System.out.println("匹配失敗"); return N; }
能夠看到,在for循環以後,直接進行匹配成功或失敗的判斷,整個過程的操做次數等於i,是小於通常方法的。
①當字符串不匹配時(這是兩種方法差別最大的地方):
使用DFA二維數組做爲有限狀態自動機,每次不匹配時都能到達精準位置(對每一個不匹配的狀況dfa都有記錄在案)。
而使用next一維數組時,在每次匹配失敗後到達的位置是不能確認的,它只是先到達可能的位置。
從可能的最長前綴位置,進行字符的匹配,若是不匹配再移到下一位可能的位置(下標在模式字符串上往前移)。
②當字符串匹配時
在兩種方式中是同樣的,i和j都加一,而後進入下一個for循環。
②最壞狀況何時出現
對於通常方法:若是文本爲AAAA,模式串爲AAAB,這時匹配到最後一位時失敗,j會一步步往前走,這時在搜索方法中操做次數達到了2n,加上構造next數組的n次操做,共3n次操做。
對於完整KMP算法:上面的狀況並不會使它達到3n,由於在j一步步往前走的時候i也會日後走,當i達到n時for循環結束,這樣最多也就操做n次,加上dfa數組的構造須要n次,共2n次操做。
結果:
能夠看到,在一般狀況下完整KMP算法的操做次數要比通常算法的操做次數少
即使是在最壞狀況下完整KMP算法的操做次數也爲通常方法的三分之二。
足以證實完整KMP的性能是更優的。
1 public class KMP { 2 private String pat; 3 private int dfa[][]; 4 5 public KMP(String pat){//由模式字符串構建dfa 6 this.pat=pat; 7 int M=pat.length(); 8 int R=256; 9 dfa=new int[R][M]; 10 dfa[pat.charAt(0)][0]=1; 11 for(int X=0,j=1;j<M;j++){//計算dfa[][j] 12 for(int c=0;c<R;c++){//不匹配狀況 13 dfa[c][j]=dfa[c][X]; 14 } 15 dfa[pat.charAt(j)][j]=j+1; 16 X=dfa[pat.charAt(j)][X]; 17 } 18 } 19 20 public int search(String txt){ 21 int N= txt.length(); 22 int M=pat.length(); 23 int j,i; 24 for(j=0,i=0;i<N&&j<M;i++){ 25 j=dfa[txt.charAt(i)][j]; 26 } 27 if(j==M){ 28 System.out.println("匹配成功"); 29 return i-M; 30 }else { 31 System.out.println("匹配失敗"); 32 return N; 33 } 34 } 35 }
測試例子:
1 @Test 2 public void KMPTest(){ 3 KMP kmp=new KMP("abc"); 4 System.out.println(kmp.search("abfeabcabc")); 5 }