KMP(Knuth–Morris–Pratt algorithm)演算法
String Searching 運用位移來處理字串搜尋問題
中譯”字串搜尋”:
兩個字串 T 和 P 找出 T 當中是否有一段字串正好是 P 並且找出位置
註:字串搜尋當中,我們通常將兩字串的象徵符號取做 T 和 P , T 意指 text , P 意指 pattern 。可以想作是從長篇文字 T 之中搜索小段文字 P 。
T: ababcabc
P: abc
ababcabc ababcabc
||| |||
abc abc
T中有兩個地方出現P
最直覺的演算法就是窮舉法:
挪動 P 對準 T 的各個位置 逐一比對字元 判斷是否相等
–> 時間複雜度為 O(TP) 。
T: ababcabc
P: abc
0. 1. 2. 3. 4. 5.
ababcabc ababcabc ababcabc ababcabc ababcabc ababcabc
||| ||| ||| ||| ||| |||
abc abc abc abc abc abc
(X) (X) (O) (X) (X) (O)
1 | void string_searching() |
failure function 窮舉法 (prefix function/ border function )–>
窮舉法的過程當中,當前比對成功的字串片段,是 P 的前綴。因為我們無法預測是 P 的哪幾個前綴,
所以我們可以預先計算 P 的每一個前綴的「次長的相同前綴後綴」,以備不時之需
failure function 是一個字串函數:輸入字串的其中一個前綴,則輸出該前綴的「次長的相同前綴後綴」。
012345
abzabc
prefix | border |failure function| implementation
-------|--------|----------------|----------------
Ø | Ø | f(Ø) = Ø |
a | Ø | f(a) = Ø | failure[0] = -1
ab | Ø | f(ab) = Ø | failure[1] = -1
abz | Ø | f(abz) = Ø | failure[2] = -1
abza | a | f(abza) = a | failure[3] = 0
abzab | ab | f(abzab) = ab | failure[4] = 1
abzabc | Ø | f(abzabc) = Ø | failure[5] = -1
計算 failure function ,一般是利用 Dynamic Programming 。
分割問題的方式,是 P[0…i] 拿掉尾端字元 P[i] ,利用已知的「次長的相同前綴後綴」,得到 P[0…i] 的「次長的相同前綴後綴」。
1 | // p[0...i] 的「次長的相同前綴後綴」是 p[0...failure[i]]。 |
稱作 failure function ,是因為比對失敗時,就會使用它。
稱作 prefix function ,是因為此函數的定義域是 prefix 。稱作 border function ,是因為此函數的值域是 border 。
Morris-Pratt Algorithm
1.
T: aabzabzabcz
P:abzabc
2.
窮舉法,從左往右逐步挪動P。
aabzabzabcz aabzabzabcz aabzabzabcz
|||||| |||||| |||||| ......
abzabc abzabc abzabc
(X) (X) (X)
2.
仔細看窮舉法 是從左往右一一比對字元 一旦發現字元不同 就馬上往右挪動P
V V V V
aabzabzabcz aabzabzabcz aabzabzabcz aabzabzabcz
| |X | ||
abzabc abzabc abzabc abzabc
V V V V
aabzabzabcz aabzabzabcz aabzabzabcz aabzabzabcz
||| |||| ||||| |||||X
abzabc abzabc abzabc abzabc
V V V V
aabzabzabcz aabzabzabcz aabzabzabcz aabzabzabcz
X X | || ......
abzabc abzabc abzabc abzabc
3.
往右挪動P之前 當下比對成功的字串片段 abzab 其實可以好好利用
V
aabzabzabcz -abzab-----
|||||X |||||
abzabc abzab-
4.
觀察窮舉法的步驟順序 繼續往右挪動P 挪動一個位置、挪動兩個位置、……
-abzab----- -abzab----- -abzab-----
|||| ||| ||
abzab- abzab- abzab-
-abzab----- -abzab-----
|
abzab- abzab-
5.
換個角度觀察上述行為
挪動一個位置:看看abzab的後四個字元 是不是前四個字元
挪動兩個位置:看看abzab的後三個字元 是不是前三個字元
6.
如果我們預先知道abzab的「次長的相同前綴後綴」是ab 就可以一口氣大幅挪動P 略過許多步驟:
V V
aabzabzabcz aabzabzabcz
|||||X ---> ||
abzabc abzabc
7.
從「V」處繼續向右一一比對字元 每當比對失敗、遇到相異字元 就故技重施
從當前比對成功的字串片段 取其「次長的相同前綴後綴」大幅挪動P
Knuth-Morris-Pratt Algorithm
v
T: aaaabcacab
P: abcabcacab
^
Morris-Pratt Algorithm 當中,當比對到上圖之處, c != b ,所以需要向右挪動 P 。
找到 abca 的「次長的相同前綴後綴」,也就是 a 。然後向右挪動 P ,最後左端凸出一段 a ,如下圖所示。
v
T: aaaabcacab
P: abcabcacab
^
接下來就繼續比對字元。在這裡 c != b ,所以又要挪動 P 了。
Knuth 則是多想了一步:連續挪動 P ,能不能預先偵測呢?
Knuth 發現,找到 abca 的「次長的相同前綴後綴」 a 之後,看看 a 的下一個字元(是 b ),是否仍是 abca 的下一個字元(也是 b )。
如果兩個字元一樣的話,那就表示 P 鐵定會連續挪動兩次,兩次比較都是 c != b 。
既然鐵定會挪動兩次,那乾脆直接移到定位不就好了?
於是 Knuth 在計算 failure function 的時候,就額外加了一個判斷:
找到 abca 的相同前綴後綴 f(abca) = a 之後,如果 f(abca) 的下一個字元恰巧仍是 abca 的下一個字元,
那麼就令 f(abca) = f(a) ,也就是再多找一次 a 的相同前綴後綴。
如此一來, P 只要移一次就行了。
由於是用遞迴定義 failure function ,所以連續挪動三次、四次、五次的情況,
經過遞迴計算,最後都會變成一次移到定位。
引用 http://www.csie.ntnu.edu.tw/~u91029/StringSearching.html#3