1. 程式人生 > >詳解KMP字串匹配演算法

詳解KMP字串匹配演算法

字串匹配

字串匹配一般是指在較長的一個字串A中查詢是否含有較短字串B、B在A中的位置的過程。最容易想到、也是最長用的一種辦法是暴力匹配。String.contains() 用的就是這種方法,應該說這種簡單的方法用的還是特廣泛的。

KMP演算法

KMP演算法俗稱“看毛片”演算法,通過計算next[] 陣列的方法加速匹配過程。

next[] 陣列:

首先要明白幾個概念,字串中的字元的前置字串(如果有),後置字串。很容易理解,前置字串就是這個字元前面的一段字串、後置字串就是這個字元後面的一串字串。
前後置字串

next[i] 陣列的定義,它反映的是i位置上的字串,它的前置字串的最長相同的字首和字尾字串的長度。前置字串應該不陌生,上面影象中 “abcdksid” 就是字元“g”的前置字串,最長相同字首和字尾字串的長度,因為“abcdksid”沒有相同的字首和字尾,所有g處next[]的值為0。例如,“abcdabc”,為3,“abcdefa”,為1。

next[i]陣列的計算。它是通過遍歷該字串的每個位置,算出每個位置處的值。第0個字元因為沒有字首字串,定義next[0]=-1; 第1個字元因為其字首字串只有一個字元,next陣列的定義要求任何子串的字尾不能包括第一個字元,也就是說沒有所謂的相同的字首字串和字尾字串,該處的值也定義為0,next[1]=0。後續next[i]值的計算,是根據前面位置處next[0] – next[i-1]的值計算出來的,如果str[i-1]==str[next[i-1]],則next[i]=next[i-1]+1。否則,比較str[i-1]與str[next[next[i-1]]]。形象說明可以參考下圖:
計算next陣列

具體程式碼如下:

public static int[] getNextArray(char[] ms){
		if(ms.length==1){
			return new int[] {-1};
		}
		int[] next=new int[ms.length];
		next[0]=-1;
		next[1]=0;
		int pos=2;
		int cn=0;
		while(pos<next.length){
			if(ms[pos-1]==ms[cn]){
				next[pos++]=++cn;
			}else if(cn>0){
				cn=next[cn];
			}else
{ next[pos++]=0; } } return next; }

這裡cn一直指向可能最長相同字串的最後一個字母。左老師這段程式碼相當精髓。

KMP遍歷方法:

求出next陣列之後,就可以根據next陣列的值,在待匹配字串上進行快速匹配。和暴力字串匹配方法(String.contains()就是用的暴力匹配)類似,只不過運用到了next陣列的值,跳過了一些匹配環節。這裡最關鍵的一個問題是,為什麼在跳過的那部分匹配中不可能找到相匹配的字串。具體證明可以用下面的圖說明,證明運用了反證法(有很多問題正面證明有困難的時候,往往反證法就會比較有用了)。
為什麼跳過next
上圖是一次匹配過程,目的是在上面字串中找到下面字串所在的位置(如果有的話),第一次匹配的時候,在A,B處發現匹配失敗,也就是說A、B前面的字串都是相同的。KMP演算法會直接向右移動A處next值的長度進行下一次匹配,現在來證明這個的合理性。假設向右移動x字串長度的位置(小於A處next值),根據上圖的推理得出矛盾。

具體程式碼如下:

public int getIndexOf(String s,String m){
		if(s==null || m==null || m.length()<1 || s.length()<m.length()){
			return -1;
		}
		char[] ss=s.toCharArray();
		char[] ms=m.toCharArray();
		int si=0;
		int mi=0;
		int[] next=getNextArray(ms);
		while(si<ss.length&&mi<ms.length){
			if(ss[si]==ms[mi]){
				si++;
				mi++;
			}else if(next[mi]==-1){
				si++;
			}else{
				mi=next[mi];
			}
		}
		return mi==ms.length? si-mi:-1;
	}

(特別感謝 左程雲 老師,他講的演算法特別透徹)