一、問題的由來
我們會在面試或者日常“搬磚”過程中遇到這類問題:有一個文本串S(比如“ababbbaccdddmmd”),有一個模式串Q(比如“baccdd”),判斷模式串Q是否是S的字串,如果是返回Q在S中的起始位置,如果不是返回-1。我們腦海裏第一個思路就是循環遍歷,如果當前字符匹配成功就繼續匹配下一個,否則S中的標記向後移動一位,Q的標記回到最開始,就會有如下代碼:
public static int kmpMatch(char[] chars, char[] ch) {
//S的長度
int sLen = chars.length;
//Q的長度
int qLen = ch.length;
int i = 0, j = 0;
while (i < sLen && j < qLen) {
if (chars[i] == ch[j]) {
i++;
j++;
} else {
//這裏是把i回到這一輪的開始位置後再向後移動一位
i = i - j + 1; // i - ( j - 1 )
j = 0;
}
}
if (j == qLen) return i - j;
else return -1;
}
這個解法是正確的,但是我們是否在每次匹配失敗的時候都需要另Q的標誌位都回到0呢?顯然是不需要,這就引入了KMP算法。
二、KMP算法
KMP算法和常規的暴力算法的區別就在於當S的字符和Q的字符不匹配時候的標記符號的移動,暴力解法是另i = i - j + 1,j = 0。在KMP算法中的核心思想是找到字符串Q中的前綴和後綴中的最長公共長度,讓i不變,j移動最長公共字串的長度,這樣就可以有效的避免了重複字符的移動,所以只需要求出j的嚇一跳的數組就可以了我這裏說的比較簡單,我特意找了我當時學習時的一篇博客供大家學習 KMP詳解,回到本文,什麼是最長公共前後綴的長度呢?
以下說明以“ABCDABD”爲例進行說明:
-"A"的前綴和後綴都爲空集,共有元素的長度爲0;
-"AB"的前綴爲[A],後綴爲[B],共有元素的長度爲0;
-"ABC"的前綴爲[A, AB],後綴爲[BC, C],共有元素的長度0;
-"ABCD"的前綴爲[A, AB, ABC],後綴爲[BCD, CD, D],共有元素的長度爲0;
-"ABCDA"的前綴爲[A, AB, ABC, ABCD],後綴爲[BCDA, CDA, DA, A],共有元素爲"A",長度爲1;
-"ABCDAB"的前綴爲[A, AB, ABC, ABCD, ABCDA],後綴爲[BCDAB, CDAB, DAB, AB, B],共有元素爲"AB",長度爲2;
-"ABCDABD"的前綴爲[A, AB, ABC, ABCD, ABCDA, ABCDAB],後綴爲[BCDABD, CDABD, DABD, ABD, BD, D],共有元素長度爲0。
所以就得到了這樣一個數組next[7]{0,0,0,0,1,2,0},所以每次只需要另j移動next[k]位就可以了,k就是每次不匹配字符的位置。於是,代碼就可以寫成如下:
private static int kmpMatch(char[] chars, char[] ch) {
int sLen = chars.length;
int pLen = ch.length;
int[] next = getNextArray(ch);
for (int i : next) {
System.out.print(i+" ");
}
int i = 0, j = 0;
while (i < sLen && j < pLen) {
if (chars[i] == ch[j]) {
i++;
j++;
} else {
j = next[j];
}
}
if (j == pLen) return i - j;
else return -1;
}
public static int[] getNextArray(char[] sub) {
int[] next = new int[sub.length];
next[0] = -1;
next[1] = 0;
int k;
for (int j = 2; j < sub.length; j++) {
//System.out.println(j);
k = next[j - 1];
while (k != -1) {
if (sub[j - 1] == sub[k]) {
next[j] = k + 1;
break;
} else {
k = next[k];
}
next[j] = 0;
}
}
return next;
}
通過getNextArray函數就可以獲得模式串Q的下一跳位置的數組,這樣就得到了KMP算法的代碼。如果還有什麼不明白的可以留言,我會進行解答的。