KMP演算法的用途
一般在字串匹配的時候,我們通常想到的就是使用KMP演算法來處理。
KMP的使用,在網上有很多例項,但是講得很讓人容易接受的不是很多,但總感覺那層窗戶紙沒有捅破,這裡我結合前人的知識,講下自己的理解,希望大家多指教。
常規暴力匹配法
有一個字串"BBC ABCDAB ABCDABCDABDE",我想知道,裡面是否包含另一個字串"ABCDABD"? 預處理時間 O(0) 時間複雜度為0(N*M)
/**
* 暴力匹配法
*
* @param text 原字串
* @param text2 要匹配的串
* @return
*/
private static int violentMatch(String text, String text2) {
int i = 0;
int j = 0;
while (i < text.length() && j < text2.length()) {
if (text.charAt(i) == text2.charAt(j)) {
i++;
j++;
} else {
i = i - j + 1;
j = 0;
}
}
if (j == text2.length()) {
return i - j;
} else {
return -1;
}
}
複製程式碼
使用KMP演算法
如題:有一個字串"BBC ABCDAB ABCDABCDABDE",我想知道,裡面是否包含另一個字串"ABCDABD"?
- 字串BBC ABCDAB ABCDABCDABDE的第一個字元與搜尋字串的第一個字元進行比較,因為B與A不匹配,所以搜尋詞後移一位。
- 因為B與A不匹配,搜尋詞再往後移。
- 就這樣,直到字串有一個字元,與搜尋詞的第一個字元相同為止。
- 接著比較字串和搜尋詞的下一個字元,還是相同。
- 直到字串有一個字元,與搜尋詞對應的字元不相同為止。
- 這時,最自然的反應是,將搜尋詞整個後移一位,再從頭逐個比較。這就是上面所展示的暴力匹配法,這樣做雖然可行,但是效率很差,因為你要把"搜尋位置"移到已經比較過的位置,重比一遍。
- 一個基本事實是,當空格與D不匹配時,你其實知道前面六個字元是"ABCDAB"。KMP演算法的想法是,設法利用這個已知資訊,不要把"搜尋位置"移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。
- 怎麼做到這一點呢?可以針對搜尋詞,算出一張《部分匹配表》。這張表是如何產生的,後面再說,這裡只要先用就可以了。
- 已知空格與D不匹配時,前面六個字元"ABCDAB"是匹配的。查表可知,最後一個匹配字元B對應的"部分匹配值"為2,因此按照下面的公式算出向後移動的位數: 移動位數 = 已匹配的字元數 - 對應的部分匹配值 因為 6 - 2 等於4,所以將搜尋詞向後移動4位。
- 因為空格與C不匹配,搜尋詞還要繼續往後移。這時,已匹配的字元數為2("AB"),對應的"部分匹配值"為0。所以,移動位數 = 2 - 0,結果為 2,於是將搜尋詞向後移2位。
- 因為空格與A不匹配,繼續後移一位。
- 逐位比較,直到發現C與D不匹配。於是,移動位數 = 6 - 2,繼續將搜尋詞向後移動4位。
- 逐位比較,直到搜尋詞的最後一位,發現完全匹配,於是搜尋完成。如果還要繼續搜尋(即找出全部匹配),移動位數 = 7 - 0,再將搜尋詞向後移動7位,這裡就不再重複了。
部分匹配表的生成
首先,要了解兩個概念:"字首"和"字尾"。 "字首"指除了最後一個字元以外,一個字串的全部頭部組合;"字尾"指除了第一個字元以外,一個字串的全部尾部組合。
即:在這種情況下,向右移動位數也可以這樣表示:已匹配字元6-上一個字元B對應的最大長度2=4,向右移動4位。 由此可得:失配時,模式串向右移動的位數為:已匹配字元數-失配字元的上一位字元所對應的最大長度值。
next陣列的求解思路
/**
* 求next陣列
* @param dest
* @return
*/
private static int[] kmpNext(String dest) {
int[] next = new int[dest.length()];
next[0] = 0;//模板字串的第一個字元最大前字尾長度固定為0
//for迴圈從第二個字元開始,依次計算每一個字元對應的next值
for (int i = 1, j = 0; i < dest.length(); i++) {
//重點,難點:遞迴求最大的相同的前字尾長度
while (j > 0 && dest.charAt(j) != dest.charAt(i)) {
j = next[j - 1];
}
//如果相等,那麼最大相同前字尾長度加1
if (dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
複製程式碼
- 已知前一步計算時最大相同的前字尾長度為j(j>0),即dest[0]到dest[j-1];
- 此時比較搜尋詞第j項與上面的字串比較
- 如果相等,那麼很簡單跳出while迴圈;
- 如果不相等,那麼只要求dest[j-1]這個位置上,即next[j-1]來求最大的相同前字尾。
KMP的整體時間複雜度為O(m + n) 附程式碼如下
class KmpTest {
public static void main(String[] args) {
String text = "BBC ABCDAB ABCDABCDABDE";
String text2 = "ABCDABD";
// System.out.println(violentMatch(text, text2));
int[] ints = kmpNext(text2);
int res = kmp(text, text2, ints);
System.out.println(res);
System.out.println("==================================");
for (int anInt : ints) {
System.out.println(anInt);
}
}
/**
* 暴力匹配法
*
* @param text
* @param text2
* @return
*/
private static int violentMatch(String text, String text2) {
int i = 0;
int j = 0;
while (i < text.length() && j < text2.length()) {
if (text.charAt(i) == text2.charAt(j)) {
i++;
j++;
} else {
i = i - j + 1;
j = 0;
}
}
if (j == text2.length()) {
return i - j;
} else {
return -1;
}
}
/**
* 求next陣列
*
* @param dest
* @return
*/
private static int[] kmpNext(String dest) {
int[] next = new int[dest.length()];
next[0] = 0;//模板字串的第一個字元最大前字尾長度固定為0
//for迴圈從第二個字元開始,依次計算每一個字元對應的next值
for (int i = 1, j = 0; i < dest.length(); i++) {
//遞迴求為最大的相同的前字尾長度
while (j > 0 && dest.charAt(j) != dest.charAt(i)) {
j = next[j - 1];
}
//如果相等,那麼最大相同前字尾長度加1
if (dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
/**
* KMP演算法
*
* @param str 文字串
* @param dest 模式串
* @param next
* @return
*/
private static int kmp(String str, String dest, int[] next) {
for (int i = 0, j = 0; i < str.length(); i++) {
while (j > 0 && str.charAt(i) != dest.charAt(j)) {
j = next[j - 1];
}
if (str.charAt(i) == dest.charAt(j)) {
j++;
}
if (j == dest.length()) {
return i - j + 1;
}
}
return 0;
}
}
複製程式碼
下面是我的公眾號,歡迎大家關注我