在看算法基礎書籍時,看到KMP算法的解釋是用的DFA(有限狀態自動機),看的我一臉懵逼。所以,就去網上搜索有沒有更容易理解的方式去實現KMP算法。
看了很多篇,感覺下面這篇博文講的比較清楚,但是也花了我挺長時間去看懂的。(好吧好吧,智商不足=_=)
后面經過自己的思考總結,在這里記錄一下自己對KMP算法的理解和實現。
KMP算法的原理
關于KMP算法的原理,上面給出的鏈接里寫的很詳細了,這里簡要的說一下。
假如在字符串 "ABCDABEFABCDABDE"
要查找 "ABCDABD"
的話,當我們遍歷匹配到最后一個字符D的時候:
ABCDAB E FABCDABDE
ABCDAB D
發現不匹配的時候,按照最容易想到的暴力算法,應該是往后移一位,再重新從頭開始進行比較:
A B C D A B E F ABCDABDE
A B C D A B D
顯然,這些一位一位的往后移的比較是沒有意義的,我們通過觀察就能知道,應該直接往后移4位,讓子字符串里的開頭兩個字符 AB
對齊原字符串,再從第三個字符 C
開始比較:
A B C D A B E F A B C DEABDE
A B C D A B D
所以重點是如何利用子字符串自己本身自帶的這些信息來幫助我們跳過一些不必要的比較。下面來分析一下 "ABCDABD"
這個字符串的特點。
假如我們在原字符串查找
"ABCDABD"
的第1個字符A
就發現不匹配,那不用說,直接往后移1位。假如匹配到了
"ABCDABD"
的第2個字符AB
發現不匹配,那還是直接往后移1位。假如匹配到了 第3,4,5個字符
ABCDA
發現不匹配,也沒有可利用的條件,那還是直接往后移1位。當我們匹配到了
"ABCDABD"
的第6個字符"ABCDAB"
的時候,發現不匹配,但是,前5個字符"ABCDA"
是已經匹配成功了的,并且結尾的字符A
與開頭的字符A
重復了.顯然我們可以移動4位,讓開頭的字符A
與結尾的字符A
對齊,再比較后面的字符是否和原字符串匹配。如下所示:
原字符串:ABCDA X???????
子字符串:ABCDA B //B與X不相等
結尾與開頭重復字符數量:1,移動4位變成
原字符串:ABCDA X ???????
子字符串: A B CDA B //是不是發現移動后,還是比較B和X是否相等?這里是不是可以改進?(現在請忽視)
- 當我們匹配到了第7個字符
"ABCDABD"
的時候,發現不匹配,而前6個字符"ABCDAB"
是已經匹配成功了的,這時我們可以還是移動4位,讓開頭的字符AB
與結尾的字符AB
對齊,再比較后面的字符C是否和原字符串后面的字符相匹配。如下所示:
原字符串:ABCDAB ???????
子字符串:ABCDAB D
結尾與開頭重復字符數量:2,移動4位變成:
原字符串:ABCDAB ? ??????
子字符串: AB C DABD //直接比較第3位的C是否和原字符串的?是否相等
說到這里,其實我們想要解決的問題就是:
在匹配失敗的時候,怎么根據已經匹配過的字符的信息來決定往后移動多少位再重新進行匹配?
所以,我們接下來要做的事就是將上面對"ABCDABD"
子字符串進行分析的過程總結出一個規律來,這也是部分匹配表
的由來。如下圖所示:
部分匹配值也就是結尾字符與開頭字符相等的數量,比如"ABCDAB"
部分匹配值就是2,"AB"
是重復的。并且可以推斷出
移動位數 = 已匹配的字符數 - 對應的部分匹配值
將這些部分匹配值存到數組里,則變成了next數組。
next數組的求解思路
next數組的求解的關鍵思想在于:
利用前面的next值去求下一個next值
舉個栗子:
如果next[i-1]對應的字符串是"ABABCABAB",此時next[i-1] = 4,代表最后4個字符"ABAB"和前4個字符是重復的。
假如next[i]對應的字符串是"ABABCABABC",即最后一個字符"C"跟上一次匹配成功的字符"ABAB"的下一個字符"C"相等,則匹配值在原來的next值上+1,即
next[i] = next[i-1]+1
假如next[i]對應的字符串是"ABABCABABD",即最后一個字符"D"跟上一次匹配成功的字符"ABAB"的下一個字符"C"不相等,我們可以觀察出來匹配值
next[i] = 0
。那是不是意味著求next[i]
的值只要看A[i]
與A[next[i-1]]
是否相等就能得出next[i]的值是next[i-1]+1或者是0了呢?假如next[i]對應的字符串是"ABABCABABA",最后一個字符"A"跟上一次匹配成功的字符"ABAB"的下一個字符"C"也不相等,但我們能觀察的出來匹配值
next[i] = 3
而不是0。這里面藏著什么貓膩呢?
實際上,next[i-1]里保存的是i-1位置的最長公共前綴后綴的長度,比如字符串ABABCABAB
,最長公共前綴后綴長度為4,也就是ABAB
。但AB
也是它的公共前綴后綴,只不過不是最長的罷了。所以,在上述的情況3
中,當最后一個字符A
匹配不成功時,我們還可以搶救一下它,退而求其次。既然想找理想的最長的公共前綴后綴失敗,那就期望一下稍短一些的公共前綴后綴去匹配,那具體是去匹配多長的字符呢?
對于位置i-1而言,公共前綴后綴的長度依次為:next[i-1], next[next[i-1]-1], next[next[next[i-1]-1]-1]......
還是以ABABCABAB
為例,next[8] = 4
, 最長公共前綴后綴為ABAB
;next[next[8]-1] = next[3] = 2
,次長公共前綴后綴為AB
;next[next[3]-1] = next[1] = 0
,說明最短的公共前綴后綴就是AB
了,長度為2。
代碼如下:
public static int[] getNext(String pattern) {
int N = pattern.length();
int next[] = new int[N + 1];
next[1] = 0;//顯然字符串的第1個字符的最大前后綴長度為0
int k =0;//最大公共前后綴長度
for (int i = 1; i < N; i++) {
while(k > 0 && pattern.charAt(i) != pattern.charAt(k))
k = next[k-1];
if(pattern.charAt(i) == pattern.charAt(k)){
k++;
}
next[i] = k;
}
return next;
}
上面代碼里可能最難理解的就是for循環里的那個while循環了。其實這個while循環就是
上面所述的去匹配公共前綴后綴的過程,如果最長next[i-1]長度的沒匹配到,就匹配稍短一點的next[next[i-1]-1],還沒匹配到,就匹配更短一點的next[next[next[i-1]-1]-1]......直到實在是找不到公共前綴后綴了,也就是長度為0的時候,就跳出循環了。
KMP算法實現
先直接貼代碼:
/**
* 在original字符串里查找子字符串find的位置
* @param original 原始字符串
* @param find 待匹配字符串
* @return 查找成功則返回匹配的首字符索引位置,否則返回-1
*/
public static int indexOf(String original, String find) {
int next[] = getNext(find);
int j = 0;
for (int i = 0; i < original.length(); i++) {
while (j > 0 && original.charAt(i) != find.charAt(j))
j = next[j-1];
if (original.charAt(i) == find.charAt(j))
j++;
if (j == find.length()) {
return i - j + 1;
}
}
return -1;
}
上面代碼里可能最不容易理解的就是內部的while循環了:
while (j > 0 && original.charAt(i) != find.charAt(j))
j = next[j];
其實這個過程就是在根據部分匹配值來移動子字符串find
的比較位置,跟我們最開始分析KMP原理的步驟是一樣的。同樣的,我們還是來舉個栗子:
假如原字符串original是AACDABEAACDAADEF
,待匹配的子字符串find是AACDAAD
在依次匹配字符的過程中,當i=5, j=5
時,出現第一次不字符不匹配:
original.charAt(5) != find.charAt(5) //即 'B' != 'A'
AACDA B EAACDAADEF
AACDA A D
這時執行循環里的語句,j = next[j] = next[5] = 1;
這就意味著再次比較original.charAt(i) != find.charAt(j)
的時候,變成了下面這樣:
AACDA B EAACDAADEF
A A CDAAD // j=1,find.charAt(j) = 'A'
這就意味著將子字符串往后移動了4位,即移動位數4 = 已匹配的字符數5 - 對應的部分匹配值1
好的,KMP算法就到此結束了。~(~ ̄▽ ̄)~
更多思考
在前面移位的時候,我們舉的栗子如下:
原字符串:ABCDA X???????
子字符串:ABCDA B //B與X不相等
結尾與開頭重復字符數量:1,移動4位變成
原字符串:ABCDA X ???????
子字符串: A B CDA B //是不是發現移動后,還是比較B和X是否相等?
可能大家看到這個栗子的時候也有點奇怪,既然移動后,還是比較B和X,可我們在移動前就已經比較過了,是不相等的。所以這里是不是可以再往后多移2位?
也就是說這里不用匹配成功了的ABCDA
的匹配值1,而是使用當前匹配失敗了的ABCDAB
的匹配值2?當然了,更多細節問題也需要考慮在內的,這只是我的一點個人想法,歡迎大家提出自己的看法、