【Java】 大話數據結構(8) 串的模式匹配算法(樸素、KMP、改進算法)

 

本文根據《大話數據結構》一書,實現了Java版的串的樸素模式匹配算法、KMP模式匹配算法、KMP模式匹配算法的改進算法html

1.樸素的模式匹配算法

  爲主串和子串分別定義指針i,j。java

    (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
BruteForce

 

2.KMP模式匹配算法

2.1 KMP模式匹配算法的主體思路

  

  在上圖的比較中,當 i 和 j 等於5時,兩字符不匹配。在樸素匹配算法中,會令i=1,j=0,而後進行下一步比較;可是,咱們其實已經知道了i=1到4的主串狀況了,沒有必要重複進行i=2到4的比較,且咱們觀察ABCABBB前面的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;
	}

  

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
KMP

 

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 位置前面字符串的相同先後綴的最大長度,咱們能夠發現abaabcc前面的字符串中相同先後綴爲ab,長度爲2,因此直接能夠選出答案爲C。

 

推薦閱讀:
  從頭至尾完全理解KMP(2014年8月22日版)

  字符串匹配的KMP算法

  超詳細理解:kmp算法next數組求解過程和回溯的含義

 

 

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
KMP2

 

   改進的算法僅在第24到28行代碼發生了改變。

 

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

 

 

附:

    要記住上面的算法,必定要記住指針 i 和 j 表明的意義,j==-1的意義,以及next的意義。

    (getNext()中前綴位置和後綴位置,index_KMP()中主串位置和子串位置),(前綴或子串的首個字符就沒法匹配),(要跳轉的下一個位置)

         還有要注意的就是,i爲後綴,咱們求的是下一個位置的next值,即next[i+1]。

相關文章
相關標籤/搜索