(閱讀本文前需要了解kmp演算法的基本思路。另外,本著大道至簡的思想,本文的所有例子都會做從頭到尾的講解)
作者翻閱了大量網上現有的kmp演算法部落格,發現廣為流傳的竟然是一種不完整的kmp演算法。
即通過next陣列來作為有限狀態自動機,以此實現非匹配時的回退。這不失為一種好的方法。
但我們接下來要見識的是一種更好和更完整的方法————擁有完整dfa的kmp演算法
在最壞情況下,對字串的操作次數僅為一般做法的三分之二。
在所有情況下,對字串的運算元都小於等於一般做法。
思路上相對於一般做法更加完整細緻,學習了它一定能讓你對kmp有乙個全新的認識。
(讀者可以在通讀全文之後回頭來看這幾句話到底對不對)
kmp演算法模擬了有限狀態自動機的執行,一般演算法中的next陣列和本文中的dfa陣列都是作為有限狀態自動機的執行指導。
有限狀態自動機不同,程式執行起來自然會存在不同。
定義:dfa=new int[r][m],r為文字可能出現的字元種類(extended_ascii的r為256位,一般情況下是夠用了),m為模式字串的長度。
空間:dfa占用空間上比next陣列大了r倍,但空間的犧牲必然要迎來效能上的提公升!
儲存內容:和next陣列一樣的是,dfa也儲存了每個位置匹配失敗時模式串的重啟位置,但它更加詳細,dfa針對了匹配失敗時可能出現的不同字元對應了其特定的重啟位置,這樣的好處在後面的效能分析中會降到。
圖1 和模式字串ababac對應的確定有限狀態機自動機
圖一展示了模式字串pat:ababac對應的確定有限狀態機自動機
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,我們要做的是:
將daf[x]複製到daf[j](對於匹配失敗的情況)
將daf[pat.charat(j)][j]設為j+1(對於匹配成功的情況)
更新x用**表示如下:
(推薦讀者先大概
看看**,再結合下面給出的完整例子,然後做**執行除錯)
dfa[pat.charat(0)][0]=1;for(int x=0,j=1;j//
計算dfa[j]
for(int c=0;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,然後和下圖對照一下是不是一樣
2、關於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。
3、例項對問題的證明:
上圖是模式abcde的dfa陣列,可以觀察到abcde中是沒有出現重複字元的,所以到最後x依然指向0
對應極端情況,前面的字元出現重複達到了四次,x也是要移動四次,但只停留在3是因為模式串已經匹配完成,不需要再移動x。
關於x的移動,是需要讀者自己在模擬dfa構造中細想的,想明白了就能全懂kmp,不明白就再看看上面的問題,嘗試自己作答就會有新的心得。
有了強大的有限狀態自動機,怎麼用它呢?實際使用中是否比原來更強大呢?咱直接將兩者的**貼出來一頓對比,順便說明精妙之處。
大體的思路是一樣的,就是將txt字串從頭到尾迴圈一遍,過程中不斷判斷模式串的位置
1、先來看看一般方法中的搜尋方法**:
for(i=0;i)if(j==-1||txt.charat(i)==pat.charat(j))
if(j==m)
}
一邊從頭到尾迴圈,一邊判斷j是不是等於m,應該注意到的是,for迴圈中還包含了乙個while,用來做回退和繼續匹配的。
可以發現,這個過程中的操作次數必定是要大於i的(每次for迴圈都可能要加入while)
2、下面是使用dfa後的搜尋方法:
for(j=0,i=0;i)if(j==m)
else
可以看到,在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的效能是更優的。
1public
class
kmp
15 dfa[pat.charat(j)][j]=j+1;
16 x=dfa[pat.charat(j)][x];17}
18}1920
public
intsearch(string txt)
27if(j==m)else34}
35 }
測試例子:
1@test
2public
void
kmptest()
字串查詢之KMP演算法
首先了解乙個概念 字首和字尾 以dbdcd為例 字首 d dbdbd dbdc 字尾 d cd dcdbdcd 首先在該演算法中具有乙個概念 部分匹配值,部分匹配值為該字串字首與字尾的重合數量,還是以上例子,dbdcd的部分匹配值為1 我們這裡先知道這個概念,後面會用到。在abcdabccabcde...
查詢字串之 KMP演算法
bf演算法中 匹配串 a a a a b c c 模式串 a a a a a b c 後移後 a a a a a b c 在位置4匹配失敗,按照bf演算法,下一輪匹配開始,匹配串指標只會向後後偏移一位,kmp 演算法是對bf演算法的改進,通過預處理,來改進每次向後的偏移量.kmp演算法實在有點繞。看...
字串查詢演算法kmp
字串查詢最簡單的方法就是乙個乙個地 滑動 查詢。這樣查詢演算法複雜度可定很高,假設pattern的長度為m,文字txt的長度為n,那麼演算法複雜度為o m n m 1 kmp模式搜尋演算法 kmp knuth morris pratt 我只認識knuth,大名鼎鼎的高納德老頭子嘛。kmp演算法的基本...