整理了一下據說由于過于晦澀難懂而導致某系統程序猿直接在實現字符串匹配的時候直接用暴力算法代替的KMP算法,初看之時確實覺得難以理解,不過經過塞得威客大大一節課的講解之后,我好像開始明白了。
其實在真實的應用中當字母表很大重復字符不多模式串很短(模式串一直都很短吧)的時候,KMP算法并不一定比暴力算法快,但是KMP算法不回退文本指針的特性使得它可以用來處理字符流中的匹配問題。而且如果像0101010111000這樣的字母表就是0,1的字符串的話KMP的算法的效率優勢就會體現出來。
這門課中的KMP算法是用有限狀態自動機(DFA)來實現的,代碼很短,但是非常的贊(高德納大神的智慧熠熠生輝),首先來看一下暴力字符串匹配法:
public class BruteForce {
public static void main(String[] args){
String txt = "ABACCABCFT";
String pat = "FT";
int a = search(pat, txt);
if (a == txt.length()){
System.out.println("Not Found!");
}else{
System.out.println("Found: " + a);
}
}
private static int search(String pat, String txt){
int M = pat.length();
int N = txt.length();
for (int i = 0; i <= N - M; i++){
int j;
for (j = 0; j < M; j++){
if (txt.charAt(i+j) != pat.charAt(j)){
break;
}
}
if (j == M) return i;//Found
}
return N;//Not Found
}
}
蠻力匹配法非常的簡單,稍微有編程基礎的人就能夠看懂。
然后是用有限狀態自動機實現的KMP算法:
public class KMP {
private final int R; // the radix
private int[][] dfa; // the KMP automoton
private String pat; // the pattern string
public static void main(String[] args) {
String pat = args[0];
String txt = args[1];
KMP kmp1 = new KMP(pat);
int offset1 = kmp1.search(txt);
// print results
System.out.println("text: " + txt);
System.out.print("pattern: ");
for (int i = 0; i < offset1; i++)
System.out.print(" ");
System.out.println(pat);
}
public int search(String txt) {
// simulate operation of DFA on text
int m = pat.length();
int n = txt.length();
int i, j;
for (i = 0, j = 0; i < n && j < m; i++) {
j = dfa[txt.charAt(i)][j];
}
if (j == m) return i - m; // found
return n; // not found
}
public KMP(String pat) {
this.R = 256;
this.pat = pat;
// build DFA from pattern
int m = pat.length();
dfa = new int[R][m];
dfa[pat.charAt(0)][0] = 1;
for (int x = 0, j = 1; j < m; j++) {
for (int c = 0; c < R; c++)
dfa[c][j] = dfa[c][x]; // Copy mismatch cases.
dfa[pat.charAt(j)][j] = j+1; // Set match case.
x = dfa[pat.charAt(j)][x]; // Update restart state.
}
}
}
可以看到search方法是幾乎一樣的,只不過是加了一個DFA,現在來看看DFA是怎么運作的:
在這個實現中,默認字母表是ANSSI字符,所以DFA中有256個小數組,ANSSI表中就256個字符,這也暴露了這種實現的一個缺點:如果是中文字母表或者是其他字特別多的表,那么需要的空間就太多了!
具體講解這個算法非常麻煩,而且塞得威客大大講的非常的好,英語好并且愛聽課的同學可以去看看他的公開課:
Coursera Algorithm Part II
里相關的視頻。
比較喜歡看文字資料的同學可以看一下我啰啰嗦嗦的講解。
KMP算法是怎么做的
我們知道,在蠻力算法中,在匹配的過程中出現了失配的話,就將模式串的指針重置為0,然后將模式串向前移動一位:
仔細觀察這張圖就能發現,這樣非常低效,我們能夠直觀地看到模式串除了第一個是B之外其他的位置上的字母都是A,在位置六失配,所以前面一段必然全是A,我們直接把模式串的起始端移動到六位置就可以了,前面的一步一步移動的操作是可以跳過的,基于這一現象,深入思考,我們可以發現,根據模式串的情況,我們可以推斷出來當在某個位置出現失配的時候我們應該怎么去移動模式串,為什么呢?因為當在六位置失配的時候,我們已經知道了第六位之前的五位是匹配的,當我們將模式串向前移動一位再進行匹配的時候,相當于讓模式串跟去掉第一個字符并左移一位的自己進行匹配(嚴格來說不是自己,是自己的一個去掉第一個字符并左移一位的自己的前綴),失配后再重復這個過程。既然是跟自己的一部分匹配,那我們可以在模式串自身進行這個過程并記錄下來各種情況出現的時候該怎么移動,那么,我們就需要構造一個有限狀態自動機了。
有限狀態自動機
有限狀態自動機是一個很簡單的概念,先簡單看一下它長什么樣子:
先不論代碼是怎么寫的,說一說背后的思想。
在KMP的字符串匹配的過程中,其實就是一個狀態機的狀態轉換問題,我們以圖上的狀態機為例。
模式串有多長,就有多少個狀態,首先我們是在0狀態,然后開始匹配第一個字符,匹配的話,狀態機進入第二個狀態,開始匹配第二個字符,不匹配的話,根據自動機里存儲的轉換方式來轉換狀態。比如在0狀態時,被匹配的長字符串的當前字母是A,那我們就根據dfa[A][0]得到我們應該轉換到1狀態,然后指向被匹配的字符串的指針進1。如果被匹配的長字符串的當前字母是B,那我們就根據dfa[B][0]得到我們應該呆在狀態0,然后指向被匹配的字符串的指針進1,然后一步一步地重復這個過程。如果我們前面一直匹配成功的話,我們會成功的從狀態0一直轉換到狀態6,當我們的狀態成功達到6的時候,就說明找到匹配了(注意這個過程中指向被匹配字符串的指針并沒有發生過回退,我們是通過狀態的轉換來決定接下來應該從模式字符串的哪一個字符開始與被匹配串的下一個字符進行匹配),匹配結束。
這個過程非常的簡單直觀,問題在于,我們怎樣才能構造出這樣的一個狀態機。
我們在這個應用中使用了一個二維數組來代表這個狀態機,數組的第一個索引是我們在被匹配字符串中所遇到的字符,第二個索引是當前自動機所在的狀態,在ANSSI字母表中,字符的總數共256個,所以共有256個子數組,在我們看到的這個案例中,模式串的長度是6,所以共有六個狀態,所以每個子數組的長度是六,代表從0到5六個狀態。
該怎么構造出這個自動機呢?
過程也非常直觀,首先我們一眼就能看出來,當處于零狀態的時候,如果我們遇到字母A,說明匹配成功,狀態機的狀態應該轉向狀態1,在狀態1的時候,如果遇到了字母B,說明匹配再次成功了,狀態機的狀態應該轉換為狀態2,按照這個思路,我們可以得到在完全匹配情況下的狀態轉換情況,于是可以在二維數組中寫下這些值。
接下來就是失配狀態了,當在狀態0的時候,如果發生失配,模式字符串就應該繼續停留在狀態0,然后將第0個字符跟被匹配串的下一個字符進行比較,所以我們可以將dfa[][0]的未匹配位置都初始化為0。
在狀態1以及以后的狀態里,情況就會稍微復雜一些,但我們可以知道這樣一件事情:如果在第五個字符失配,那我們起碼知道下一次要輸入自動狀態機的四個字符是啥,有了這個,就可以不用回退文本指針了。
我們現在要做的事情是這樣的,首先用一個指針x指向0狀態,然后我們可以直接把0狀態里的數值直接拷貝到1狀態,因為當在一狀態的時候發生不匹配的時候,我們會用已匹配串的去掉首字母前綴進行匹配,結果是空的,因為此時就一個字母匹配,然后我們再用不匹配的那個字母跟模式字符串的第一個字母去匹配,得到模式應該處于的狀態,所以可以直接將第一個字母的對應的狀態機的這一列拷貝過來,直接拿到結果。然后這個過程是迭代的,也就是說,當我們在模式串的第三個字符失配的時候,我們實際上是在拿模式串的第二個字符(因為模式串這時肯定要向前進一)去跟模式串的0狀態匹配,然后得到我們用來進行匹配的下一個狀態,我們已經通過x = dfa[pat.charAt(j)][x]得到了輸入模式串的第二個字符后自動機的狀態,也就是說x現在指向的那一列狀態就是當前待匹配字符輸入之后應該轉換到的狀態了(也就是說,在當前位置失配了,當前位置的文本串應該去跟x位置的模式串字符去做匹配),所以我們可以把x指向的那一列直接抄過來。
匹配進狀態進一,不匹配把去掉首字母已匹配串的的前綴輸入狀態機(這個操作在構造狀態機的過程中迭代進行,沒有重復操作),這樣一個狀態機就構造出來了。
有了這樣一個狀態機,在進行模式匹配的時候,我們就可以用j = dfa[txt.charAt(i)][j]; 變換自動機的狀態,當j等于最后一個狀態的時候,就說明匹配成功了。
KMP算法就這么煉成了
理解了有限狀態自動機,KMP算法也就可以很容易地寫出來了,為了練習一下,筆者做了一下leetcode里的編程練習:
字符串匹配
有興趣的同學可以去練習一下加深理解。
后記:反復review加跟各種人解釋,我特么的把這段代碼背下來了