本文根據《大話數據結構》一書,實現了Java版的串的樸素模式匹配算法、KMP模式匹配算法、KMP模式匹配算法的改進算法。html
爲主串和子串分別定義指針i,j。java
(1)當 i 和 j 位置上的字母相同時,兩個指針都指向下一個位置繼續比較;算法
(2)當 i 和 j 位置上的字母不一樣時,i 退回上次匹配首位的下一位,j 則返回子串的首位。數組
實現程序:數據結構
/** * 樸素的模式匹配算法 * 說明:下標從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
在上圖的比較中,當 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位置不匹配時要跳轉的下一個進行比較的位置。ide
KMP模式匹配算法:函數
爲主串和子串分別定義指針 i 和 j 。google
(1)當 i 和 j 位置上的字母相同時,兩個指針都指向下一個位置繼續比較;spa
(2)當 i 和 j 位置上的字母不一樣時,i 不變,j 則返回到next[j]位置從新比較。(暫時先無論next[]的求法,只要記得定義有next[0]=-1).net
(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; }
根據上述內容可知,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; }
結合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
已知字符串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日版)
對於以下字符串,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]。