1. 程式人生 > >字串匹配演算法——KMP && BF

字串匹配演算法——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實現版本:

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;
    }
上述程式碼中沒有使用另外一個變數 i 來記錄位移位置, 而是使用k記錄位移位置,所以k回退到k = k - j + 1 位置再進行匹配。該演算法較簡單,就不多說了,下面重點講講KMP演算法。

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 ] 時,源串就應該向後移動一個字元。 


朋友們使用過程中若發現出問題了,別忘了告訴我一聲哦^_^... 

關於字串匹配的演算法暫時就寫到這裡,更多內容後續再補充。