KMP算法(字符串匹配算法)詳解及java實現

KMP算法是BF(Brute Force)算法的一種改進算法,什麼是BF算法這裏不多做解釋。

1.KMP算法實現思路:

  每當一趟匹配過程中出現字符比較不等時,不需要回溯主串上面的指針i而是利用已經計算出的模式串P在j位置前面的子串P0...Pj-1部分匹配值k將模式向右滑j-k個字符,然後繼續進行比較。
 

2.理解"前綴"、"後綴"和“部分匹配值”的概念

  首先這裏要引入"前綴"和"後綴"的概念(這個很重要),

  (1)前綴:指除了最後一個字符以外,一個字符串的全部頭部組合;

  (2)後綴:指除了第一個字符以外,一個字符串的全部尾部組合;

  (3)部分匹配值:就是"前綴"和"後綴"的最長的共有元素的長度,如以字符串"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。

3.下面開始具體解析KMP:
 
假設主串S的長度爲n,模式串P的長度爲m,i爲主串S當前位置的指針,j爲模式串P當前位置的指針:
  S0.....Si-jSi-j+1Si-j+2.......Si-2Si-1...........Sn
        P0 P1 P2...............Pj-2Pj-1 

即:Si-jSi-j+1Si-j+2...Si-1=P0 P1 P2...Pj-2Pj-1                      (1-1)

當Si!=Pji不動,模式串P向右移動多少個字符最正確(即要保證不會漏掉可能的匹配或不會重複不必要的匹配過程)

如果P本身的每一個字符都不相同,那麼就可以直接將模式串P向右移動j個字符,道理很簡單因爲P0!=P1!=P2...!=Pj-1,由上面等式(1-1)可知P0也不等於Si-jSi-j+1Si-j+2.......Si-2Si-1中的任何一個,所以可以直接從P0開始和Si進行下一輪比較(指針i不需要回溯,指針j回溯到模式串的起始位置)。

但是如果模式串P存在很多重複的字符如:abcabcabd這種情況時就不能直接將j指針移動到P0了,例如主串爲fffffabcabcabcabcabdfffff時

               i       

         fffffabcabcabcabcabdfffff

          abcabcabd

              j

              ↑ 發現 c != d 即 S!= Pj

此時應該怎麼移動呢?如果直接將j移動到P0然後和Si比較則會出現漏掉匹配的情況即匹配結束後找不到匹配串,正確的做法是將j—>P5位置(相當於向右滑動3個位置)然後和Si繼續比較,如下所示:

               i       

         fffffabcabcabcabcabdfffff

               abcabcabd

              j

爲什麼是移動到P5呢?這個P5是怎麼來的?這個就是整個算法的關鍵點,理解了這一點也就理解了KMP算法的本質。

其實這個5就是Pj-1的部分匹配值k,移動字符個數=j-k=8-5=3(j=8,k=5)

根據上面字符串部分匹配值的定義可知當j=8時P0P1...Pj-1等於字符串abcabcab,該字符串的前綴和後綴的最長共有元素的長度爲5,即abcabca和bcabcab重疊的部分最大長度爲5。

那麼這是什麼原理呢?爲什麼P0P1...Pj-1的部分匹配值就是模式P在位置j失配時重新開始匹配的位置呢?爲什麼不需要回溯i指針及完全回溯j指針到P0,卻不會出現漏掉匹配或者怎麼能確保這種情況下是沒有進行不必要的重複匹配呢?

下面去看分析:

當在j位置失配時有 P!= S且等式 Si-jSi-j+1Si-j+2...Si-1=P0 P1 P2...Pj-2Pj-1 必定成立

又由字符串部分匹配值的定義可知P0P1...Pk-1=Pj-kPj-k+1...Pj-1,上面的列子中即P0P1P2P3P4=P3P4P5P6P7(j=8,k=5)

因爲:Pj-kPj-k+1...Pj-1=Si-kSi-k+1...Si-1,所以P0P1...Pk-1=Si-kSi-k+1...Si-1

前綴和後綴的最長共有元素的意思就是說當y>k時不可能存在Pj-yPj-y+1...Pj-1=P0P1P2...Pj-y-1(這裏是關鍵,y就是該字符串的某一個前綴和後綴的長度,k是該字符串的部分匹配值,所以不可能存在一個y>k使得等式成立),只有當y=<k時等式纔會成立;因此可以推斷出:

P0P1P2...Pj-y-1和Si-j+1Si-j+2Si-j+3...Si-1進行匹配時前面j-k次都不會匹配成功,這就是KMP算法中當失配時直接將模式串P向右滑動k個字符的原理。

模式串P的部分匹配值表怎麼求,下篇博文裏面再詳細說明,其實關鍵點還是前綴和後綴以及部分匹配值的問題,把這個搞懂了就都懂了。


具體實現:


public class KMP {

	void getNext(String pattern, int next[]) {
		int j = 0;
		int k = -1;
		int len = pattern.length();
		next[0] = -1;

		while (j < len - 1) {
			if (k == -1 || pattern.charAt(k) == pattern.charAt(j)) {

				j++;
				k++;
				next[j] = k;
			} else {

				// 比較到第K個字符,說明p[0——k-1]字符串和p[j-k——j-1]字符串相等,而next[k]表示
				// p[0——k-1]的前綴和後綴的最長共有長度,所接下來可以直接比較p[next[k]]和p[j]
				k = next[k];
			}
		}

	}

	int kmp(String s, String pattern) {
		int i = 0;
		int j = 0;
		int slen = s.length();
		int plen = pattern.length();

		int[] next = new int[plen];

		getNext(pattern, next);

		while (i < slen && j < plen) {

			if (s.charAt(i) == pattern.charAt(j)) {
				i++;
				j++;
			} else {
				if (next[j] == -1) {
					i++;
					j = 0;
				} else {
					j = next[j];
				}

			}

			if (j == plen) {
				return i - j;
			}
		}
		return -1;
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		KMP kmp = new KMP();
		String str = "abababdafdasabcfdfeaba";
		String pattern = "abc";
		System.out.println(kmp.kmp(str, pattern));
	}

}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章