字串匹配演算法——KMP && BF
字串匹配操作定義:
目標串S="S0S1S2...Sn-1" , 模式串T=“T0T1T2...Tm-1”
對合法位置 0<= i <= n-m (i稱為位移)依次將目標串的字串 S[i ... i+m-1] 和模式串T[0 ... m-1] 進行比較,若:
1、S[i ... i+m-1] = T[0 ... m-1] , 則從位置i開始匹配成功,稱模式串 T 在目標串 S 中出現。
2、S[i ... i+m-1] != T[0 ... m-1] ,則從位置i開始匹配失敗。
1、字串匹配演算法一 —— Brute-Force 演算法(很黃很暴力)
字串匹配過程中,對於位移i (i在目標串中的某個位置),當第一次 Sk != Tj 時,i 向後移動1位 , 及 i = i+1,此時k退回到i+1位置 ;模式串要退回到第一個字元。該演算法時間複雜度O(M*N),但是實際情況中時間複雜度接近於O(M + N),以下為Brute-Force演算法的java實現版本:
上述程式碼中沒有使用另外一個變數 i 來記錄位移位置, 而是使用k記錄位移位置,所以k回退到k = k - j + 1 位置再進行匹配。該演算法較簡單,就不多說了,下面重點講講KMP演算法。public static int bruteForce(String target, String pattern, int pos) { if (target == null || pattern == null) { return -1; } int k = pos - 1, j = 0, tLen = target.length(), pLen = pattern.length(); while (k < tLen && j < pLen) { if (target.charAt(k) == pattern.charAt(j)) { j++; k++; } else { k = k - j + 1; j = 0; } } if (j == pLen) { return k - j + 1; } return -1; }
2、字串匹配演算法二 —— KMP(D.E.Knuth 、J.H.Morris 和 V.R.Pratt)演算法
其思想是每一次出現不匹配的字元時,儘可能的向前滑動位移,而這個滑動的位移取決於模式串(暫且稱為next[j]),nextj的定義
下面運用上述原理求解模式串 T=“abcdsaaabcdsabbc”,next[1]=0;第一個字元始終為0。
對於位置 j = 2 , Tj = b,由於k不可能大於1 , 滿足第三種情況:
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 |
對於位置 j = 3 , j前面的字串T' = "ab",不存子序列T'' 開始的字串相等 ,所以next[3]=1
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 |
同理對於位置 j = 4,5,6 ; next[j] = 1 :
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 | 1 | 1 | 1 |
對於位置 j = 7 , Tj = a , j前面的字串T' = "abcdsa",存在一個最大長度為1的字串和開始的字串相等,所以next[7] = 2:
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 | 1 | 1 | 1 | 2 |
同理對於位置 j = 8,9 ; next[j] = 2 :
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 |
對於位置 j = 10 , j 前面的字串 T' = "abcdsaaab", 存在一個最大長度為2的子序列(ab)和開始的字串相等,所以next[10] = 3 :
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 3 |
對於位置 j = 11 , j 前面的字串 T' = "abcdsaaabc", 存在一個最大長度為3的子序列(abc)和開始的字串相等,所以next[11] = 4 :
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 3 | 4 |
對於位置 j = 12 , j 前面的字串 T' = "abcdsaaabcd", 存在一個最大長度為4的子序列(abcd)和開始的字串相等,所以next[12] = 5 :
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 3 | 4 | 5 |
對於位置 j = 13 , j 前面的字串 T' = "abcdsaaabcds", 存在一個最大長度為5的子序列(abcds)和開始的字串相等,所以next[13] = 6 :
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 3 | 4 | 5 | 6 |
對於位置 j = 14 , j 前面的字串 T' = "abcdsaaabcdsa", 存在一個最大長度為6的子序列(abcdsa)和開始的字串相等,所以next[14] = 7 :
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 3 | 4 | 5 | 6 | 7 |
對於位置 j = 15 , j 前面的字串 T' = "abcdsaaabcdsab", 存在一個最大長度為2的子序列(ab)和開始的字串相等,所以next[15] = 3 :
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 3 | 4 | 5 | 6 | 7 | 3 |
對於位置 j = 16 , j 前面的字串 T' = "abcdsaaabcdsabb", 不存在子序列和開始的字串相等,所以next[16] = 1 :
a | b | c | d | s | a | a | a | b | c | d | s | a | b | b | c |
0 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 3 | 4 | 5 | 6 | 7 | 3 | 1 |
上述按演算法思想按照人類的語言來組織求解了,但是對於更長的模式串nextj又該如何求呢?肯定不會是人工來算的啦^_^,那麼計算機如何來求這個nextj函式呢?求解nextj的原理其實這裡面http://www.56.com/u59/v_NjAwMzA0ODA.html已經有了,簡單來說就是:
對於T[j] = k , 則說明對於j前面有一個字串 T [1,2...k-1] =T [ j-k+1 , j-k+2....j-1]相等。
如果 T[j] = T[k] , 那麼必定存在一個字串 T[1,2,...k-1,k] = T [j-k+1 , j-k+2 ... j-1,j]相等。
則==> T[j+1] = k+1 ;
例如上面例子中的 T[10] = 3
j =10 , k = 3 , 又T10 = T3=c,所以 T[11] =4 ;
如果T[j] != T[k] , 則需要回溯,T[j] = T[?]
例如上例中的 T[14] = 7 , j=14 , k=7 , 但是T7 =a ,T14 = b , a != b , 此時就需要回溯,怎麼回溯呢? j 不變 , k 回溯到 T[k] , k=7 ,
k = T[7] = 2 ,
再判斷 Tj 是否等於 Tk , T14 = b , T2 = b ; ==>T15 = t2
所以 T[15] = T[2] +1 ; ==> T[15] = 3
再來看看上例中的T[15] = 3, j =15 , k =3 ;
T3 = c , T15 = b ; ==> T3 != T15
回溯,k = T[k] = T[3] , T[3] = 1 ; ==> k = 1 ;
但是T15 = b 不等於 T1 = a ;
繼續回溯: k = T[1] = 0 ;
通過nextj的定義可以知道只有第一個字元的nextj值為0 , 此時表明已經回溯到了第一個字元,此時 k 和 j 都要向後移動一位。
下面給出nextj 的Java實現程式碼:
private static int[] next(String t) {
String s = " " + t;
int k = 1, j = 0, sLen = s.length();
int real_next[] = new int[t.length()];
int next[] = new int[sLen];
while (k < sLen) {
if (j == 0 || s.charAt(k) == s.charAt(j)) {
k++;
j++;
if (k >= sLen)
break;
next[k] = j;
// nextj 函式的優化部分
// if (s.charAt(k) != s.charAt(j)) {
// next[k] = j;
// } else {
// next[k] = next[j];
// }
// 優化程式碼結束
}
else {
j = next[j];
}
}
System.arraycopy(next, 1, real_next, 0, real_next.length);
// for (int i = 0; i < real_next.length; i++) {
// System.out.print(real_next[i] + " ");
// }
// next = null;
return real_next;
}
對上述程式碼採用了模擬現實操作的過程,構造了一個臨時的長度為t.len +1的字串,對其求出nextj後對應回t的相應位置。在上述程式碼中看到了對next部分優化的程式碼,為什麼要優化呢?上述連結的視訊中講得很清楚了。具體說就是要考慮源字串了:
對於源串S ,模式串T :
如果Si != Tj , 那麼此時 j 就會回溯到 位置 k 上 , k = T[j] (上述例子就是這種方法啦 )。
但是如果 Tj = Tk , ==> Si != Tk
此時 就該比較 k' , k' = T[k] , 這麼說來 比較 Si 和 Tk 相當於是脫了褲子放屁,多此一舉。我們就應該直接用 Si 和 T[k'] 進行比較
即是: Si = Tk' , k' = T[ T[j] ] ==> T[j] = T[ T[j] ];
nextj 函式求解完了, 下面接著看 KMP演算法
KMP搜尋部分的程式碼網上也很多了, 其程式碼結構和 Brute-Force 搜尋演算法的程式碼結構類似,下面直接看程式碼:
/**
* 字串匹配,KMP演算法
*
* @param s 源字串
* @param t 匹配的目標字串
* @param pos 匹配字串的初始位置 , pos = 1, 2, .... , s.len
* @return 模式串在源字串中從pos位置開始搜尋第一次出現的位置
*/
public static int stringMatchKmp(String s, String t, int pos) {
if (s == null || t == null || s.length() < t.length() + pos) {
return -1;
}
int k = pos - 1, j = 0, tLen = t.length(), kPos = s.length() - tLen;
int[] nextj = next(t);
while (k <= kPos && j < tLen) {
if (j == 0 || s.charAt(k) == t.charAt(j)) {
k++;
j++;
} else {
if(j == nextj[j]){
k++;
}
j = nextj[j];
}
}
if (j >= tLen) {
return k - tLen + 1;
}
return -1;
}
關於回溯部分的程式碼添加了一句:if(j == nextj[j]){
k++;
}
對某些特殊請況做處理,比如既不滿足if條件 同時 j = nextj[ j ] 時,源串就應該向後移動一個字元。
朋友們使用過程中若發現出問題了,別忘了告訴我一聲哦^_^...
關於字串匹配的演算法暫時就寫到這裡,更多內容後續再補充。