本文根據《大話數據結構》一書,實現了Java版的串的朴素模式匹配算法、KMP模式匹配算法、KMP模式匹配算法的改進算法。
1.朴素的模式匹配算法
為主串和子串分別定義指針i,j。
(1)當 i 和 j 位置上的字母相同時,兩個指針都指向下一個位置繼續比較;
(2)當 i 和 j 位置上的字母不同時,i 退回上次匹配首位的下一位,j 則返回子串的首位。
(注:該圖從下標為1開始 )
實現程序:
/**
* 朴素的模式匹配算法
* 說明:下標從0開始,與書稍有不同,但原理一樣
* @author Yongh
*
*/
public class BruteForce {
/*
* 返回子串t在主串s中第pos個字符后的位置。若不存在返回-1
*/
int index(String s,String t,int pos) {
int i=pos; //i為主串位置下標
int j=0; //j為子串位置下標
while(i<s.length()&&j<t.length()) {
if(s.charAt(i)==t.charAt(j)) {
i++;
j++; //i和j指向下一個位置繼續比較
}else { /*重新匹配*/
i=i-j+1; //退回上次匹配首位的下一位
j=0; //返回子串的首位
}
}
if(j==t.length()) {
return i-j;
}else {
return -1;
}
}
public static void main(String[] args) {
BruteForce sample =new BruteForce();
int a= sample.index("goodgoogle", "google", 0);
System.out.println(a);
}
}
4
2.KMP模式匹配算法
2.1 KMP模式匹配算法的主體思路

在上圖的比較中,當 i 和 j 等於5時,兩字符不匹配。在朴素匹配算法中,會令i=1,j=0,然后進行下一步比較;但是,我們其實已經知道了i=1到4的主串情況了,沒有必要重復進行i=2到4的比較,且我們觀察“ABCABB”的B前面的ABCAB,其前綴與后綴(黃色部分)相同,所以可以直接進行上圖中的第三步比較(令 i 不變,令 j 從5變成2,繼續進行比較)。這就是KMP模式匹配算法的大概思路。這當中的 j 從5跳轉到了2,2通過一個函數next(5)求得,next(5)即代表j=5位置不匹配時要跳轉的下一個進行比較的位置。
KMP模式匹配算法:
為主串和子串分別定義指針 i 和 j 。
(1)當 i 和 j 位置上的字母相同時,兩個指針都指向下一個位置繼續比較;
(2)當 i 和 j 位置上的字母不同時,i 不變,j 則返回到next[j]位置重新比較。(暫時先不管next[]的求法,只要記得定義有next[0]=-1)
(3)當 j 返回到下標為0時,若當 i 和 j 位置上的字母仍然不同,根據(2),有 j = next[0]=-1,這時只能令 i 和 j 都繼續往后移一位進行比較 (同步驟(1))。
上述內容可結合下圖說明:

(1)i 和 j 從下標為0開始比較,該位置兩字母相同,i 和 j 往后移繼續比較;
(2)一直比較到 i 和 j 等於5時,兩字母不同, i 不變,j 返回到 next[j]的位置重新比較,該子串的next[5]=2,所以 j 返回到下標為2的位置繼續與 i=5的主串字母比較。
(3)在下圖情況下,當j=0時,兩字母不同,子串只能與主串的下一個元素比較了(即i=1與j=0比較)。根據(2),會使 j=next[j]=next[0]=-1,所以現在的i=0,j=next[0]=-1了,要下一步比較的話兩個指針都要加一。

根據上述說明可以寫出如下代碼(代碼中的next[]暫時假設已知,之后會講):
/*
* 返回子串t在主串s中第pos個字符后的位置(包含pos位置)。若不存在返回-1
*/
public int index_KMP(String s, String t, int pos) {
int i = pos; //主串的指針
int j = 0; //子串的指針
int[] next = getNext(t); //獲取子串的next數組
while (i < s.length() && j < t.length()) {
if (j == -1 || s.charAt(i) == t.charAt(j)) {
// j==-1說明了子串首位也不匹配,它是由上一步j=next[0]=-1得到的。
i++;
j++;
} else {
j = next[j];
}
}
if (j == t.length())
return i - j;
return -1;
}
2.2 next[]的定義與求解
根據上述內容可知,next[j] 的含義為:當下標為 j 的元素在不匹配時,j 要跳轉的下一個位置下標。
繼續結合下圖說明:

當j=5時,元素不匹配,j跳轉到next[5]=2的位置重新比較。
那為什么next[5]的值為2呢?即,為什么j=5不匹配時要跳轉到2位置呢?
觀察 ABCABB 這個字符串,下標為5的字符為B,它前面的字符 ABCAB 與主串完全相同,而ABCAB的前綴與后綴(黃色部分)相同,,所以前綴AB不用再進行比較了,直接比較C這個字符,即下標為2的字符,所以next[5]=2。
那么該如何求解跳轉位置next[]呢?通過剛才的討論,我們可以發現next[j]的值等於 j 位置前面字符串的相同前后綴的最大長度,上面例子就是等於AB的長度2。
next[]的公式如下:

公式說明:
1.在j=0時,0位置之前沒有字符串,next[0]定義為-1 ;
2. 在 j 位置之前的字符串中,如果有出現前后綴相等的情況,令 j 變為相等部分的最大長度,即剛剛所說的相同前后綴的最大長度。如上述的ABCABB字符串中,j=5時,前面相等部分AB長度為2,所以next[5]=2;
3.其余情況下,next[j]=0。其他情況,沒有出現字符的前后綴相等,相同前后綴的最大長度自然就是0。
那求解next[]的代碼如何實現呢?以下是代碼的分析過程:
1.定義兩個指針 i=0 和 j=-1,分別指向前綴和后綴( j 值始終要比 i 值小),用於確定相同前后綴的最大長度;(因為 i 是后綴,所以我們求的都是 i+1位置的next值next[i+1])
2.根據定義有:next[0]=-1;
3.當前綴中 j 位置的字符和后綴中 i 位置的字符相等時,說明 i+1 位置的next值為 j+1 (因為 j+1 為相同前后綴的最大長度,可結合下面兩種情況思考)(即next[i+1]=j+1 )

4.j==-1時,說明前綴沒有與后綴相同的地方,最大長度為0,則 i+1 位置的next值只能為0,此時也可以表示為next[i+1]=j+1。
5.當 j 位置的字符和 i 位置的字符不相等時,說明前綴在第 j 個位置無法與后綴匹配,令 j 跳轉到下一個匹配的位置,即 j= next[j] 。
以下是實現求解next[]的程序:
/*
* 返回字符串的next數組
*/
public int[] getNext(String str) {
int length = str.length();
int[] next = new int[length]; //別忘了初始化
int i = 0; //i為后綴的指針
int j = -1; //j為前綴的指針
next[0] = -1;
while (i < length - 1) { // 因為后面有next[i++],所以不是i<length
if (j == -1 || str.charAt(i) == str.charAt(j)) { // j == -1代表前后綴沒有相等的部分,i+1位置的next值為0
next[++i] = ++j; //等於前綴的長度
} else {
j = next[j];
}
}
return next;
}
2.3 KMP完整代碼
結合next數組的求解和KMP算法,完整代碼如下:
import java.util.Arrays;
/**
* KMP模式匹配算法
* 返回子串t在主串s中第pos個字符后的位置。若不存在返回-1 要注意i不變,只改變j
*
* @author Yongh
*
*/
public class KMP {
/*
* 返回字符串的next數組
*/
public int[] getNext(String str) {
int length = str.length();
int[] next = new int[length]; //別忘了初始化
int i = 0; //i為后綴的指針
int j = -1; //j為前綴的指針
next[0] = -1;
while (i < length - 1) { // 因為后面有next[i++],所以不是i<length
if (j == -1 || str.charAt(i) == str.charAt(j)) { // j == -1代表前后綴沒有相等的部分,i+1位置的next值為0
next[++i] = ++j; //等於前綴的長度
} else {
j = next[j];
}
}
return next;
}
/*
* 返回子串t在主串s中第pos個字符后的位置(包含pos位置)。若不存在返回-1
*/
public int index_KMP(String s, String t, int pos) {
int i = pos; //主串的指針
int j = 0; //子串的指針
int[] next = getNext(t); //獲取子串的next數組
while (i < s.length() && j < t.length()) {
if (j == -1 || s.charAt(i) == t.charAt(j)) {
// j==-1說明了子串首位也不匹配,它是由j=next[0]=-1得到的。
i++;
j++;
} else {
j = next[j];
}
}
if (j == t.length())
return i - j;
return -1;
}
public static void main(String[] args) {
KMP aKmp = new KMP();
System.out.println(Arrays.toString(aKmp.getNext("BBC")));
System.out.println(Arrays.toString(aKmp.getNext("ABDABC")));
System.out.println(Arrays.toString(aKmp.getNext("ababaaaba")));
System.out.println(aKmp.index_KMP("goodgoogle", "google", 0));
}
}
[-1, 0, 1] [-1, 0, 0, 0, 1, 2] [-1, 0, 0, 1, 2, 3, 1, 1, 2] 4
2.4 一道題目
已知字符串S為abaabaabacacaabaabcc,模式串P為abaabc。采用KMP算法進行匹配,第一次出現“失配”(S[i]≠P[j])時,i=j=5,則下次開始匹配時,i和j的值分別是:C。 A. i = 1, j = 0 B. i = 5, j = 0 C.i = 5, j = 2 D. i = 6, j = 2
分析:模式串就是之前所說的子串,i 和 j 是之前所說的指針。根據剛剛的分析中,出現失配時,指針 i 是不會變動的,只會變 j,j=next[j]。next[j]的物理意義是 j 位置前面字符串的相同前后綴的最大長度,我們可以發現abaabc中c前面的字符串中相同前后綴為ab,長度為2,所以直接可以選出答案為C。
推薦閱讀:
從頭到尾徹底理解KMP(2014年8月22日版)
3.KMP模式匹配算法改進
對於如下字符串,j=3時,next[j]=1,根據next的定義,即當 j=3位置不匹配時,j跳轉到1位置重新比較,但可以發現,j=2位置和j=1位置其實是同一個字母,沒有必要重復比較。

舉個例子,在KMP算法下的比較過程如下(按圖依次進行):

因為有next[3]=1,所以會出現中間這個其實可以省略掉的過程。實際上我們是可以直接跳到j=0那一步進行比較的,這就需要修改next數組,我們把新的數組記為nextval數組。
中間那步可以省略是因為,j=3和 j=1位置上的字符是完全相同的,因此沒有必要再進行比較了。因此只需要在原有的next程序中加上一個字符是否相等的判斷,如果要跳轉的nextval位置上的字符於當前字符相等,令當前字符的nextval值等於要跳轉位置上的nextval值。

KMP模式匹配算法的改進程序如下:
import java.util.Arrays;
/**
* KMP模式匹配算法 的改進算法
* 返回子串t在主串s中第pos個字符后的位置。若不存在返回-1 要注意i不變,只改變j
*
* @author Yongh
*
*/
public class KMP2 {
/*
* 返回字符串的next數組
*/
public int[] getNextval(String str) {
int length = str.length();
int[] nextval = new int[length];
int i = 0; //i為后綴的指針
int j = -1; //j為前綴的指針
nextval[0] = -1;
while (i < length - 1) {
if (j == -1 || str.charAt(i) == str.charAt(j)) {
i++;
j++;
if(str.charAt(i)!=str.charAt(j)) { //多了一個字符是否相等的判斷
nextval[i] = j; //等於前綴的長度
}else {
nextval[i]=nextval[j];
}
} else {
j = nextval[j];
}
}
return nextval;
}
/*
* 返回子串t在主串s中第pos個字符后的位置(包含pos位置)。若不存在返回-1
*/
public int index_KMP(String s, String t, int pos) {
int i = pos; //主串的指針
int j = 0; //子串的指針
int[] next = getNextval(t); //獲取子串的next數組
while (i < s.length() && j < t.length()) {
if (j == -1 || s.charAt(i) == t.charAt(j)) {
// j==-1說明了子串首位也不匹配,它是由j=next[0]=-1得到的。
i++;
j++;
} else {
j = next[j];
}
}
if (j == t.length())
return i - j;
return -1;
}
public static void main(String[] args) {
KMP2 aKmp = new KMP2();
System.out.println(Arrays.toString(aKmp.getNextval("BBC")));
System.out.println(Arrays.toString(aKmp.getNextval("ABDABC")));
System.out.println(Arrays.toString(aKmp.getNextval("ababaaaba")));
System.out.println(aKmp.index_KMP("goodgoogle", "google", 0));
}
}
[-1, -1, 1] [-1, 0, 0, -1, 0, 2] [-1, 0, -1, 0, -1, 3, 1, 0, -1] 4
改進的算法僅在第24到28行代碼發生了改變。

圖中這句話可以結合下表仔細體會。(要記得nextval[j]的含義:j位置的字符未匹配時要跳轉的下一個位置)

附:
要記住上面的算法,一定要記住指針 i 和 j 代表的意義,j==-1的意義,以及next的意義。
(getNext()中前綴位置和后綴位置,index_KMP()中主串位置和子串位置),(前綴或子串的首個字符就無法匹配),(要跳轉的下一個位置)
還有要注意的就是,i為后綴,我們求的是下一個位置的next值,即next[i+1]。
