詳解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]]]。形象說明可以參考下圖:
具體程式碼如下:
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陣列的值,跳過了一些匹配環節。這裡最關鍵的一個問題是,為什麼在跳過的那部分匹配中不可能找到相匹配的字串。具體證明可以用下面的圖說明,證明運用了反證法(有很多問題正面證明有困難的時候,往往反證法就會比較有用了)。
上圖是一次匹配過程,目的是在上面字串中找到下面字串所在的位置(如果有的話),第一次匹配的時候,在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;
}
(特別感謝 左程雲 老師,他講的演算法特別透徹)