字符串的模式匹配:KMP算法

  KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,因此人們稱它爲克努特——莫里斯——普拉特操作(簡稱KMP算法)。KMP算法的關鍵是利用匹配失敗後的信息,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是實現一個next()函數,函數本身包含了模式串的局部匹配信息。其實KMP算法與BF算法的區別就在於KMP算法巧妙的消除了指針i的回溯問題,只需確定下次匹配j的位置即可,使得問題的複雜度由O(mn)下降到O(m+n)。

與BF算法的比較

  普通的匹配算法BF中,目標串和模式串的索引值都需要重新回到起點再次進行匹配,所以該算法的時間複雜度較高,達到O(m*n)。KMP算法的核心思想是尋找模式串本身的特徵,在此基礎上達到目標串不回溯,模式串有規律回溯的目的,以減少回溯比較的次數,降低時間複雜度,達到O(m+n)。但是這是建立在提高空間複雜度的基礎上的。
  BF算法:時間複雜度O(m*n);空間複雜度O(1)
  KMP算法:時間複雜度O(m+n);空間複雜度O(n)

算法思想

  KMP算法的核心是尋找模式串本身的規律。在該算法中表現爲反映該規律的next數組。next數組的作用是在每次失配時,模式串根據next數組對應位置上的值回溯模式串索引的位置。next數組的求法也是KMP算法的精華所在。
  
  next數組有如下定義:

(1) next[j] = -1: j = 0
(2) next[j] = max(k): 0 < k < j 且 P[0,k-1]=P[j-k, j-1];
(3) next[j] = 0 其他

  該定義的意義就是next數組的首位均爲-1。在其他位置上,該位置之前的子串存在與該位置之後的同等長度的子串,我們稱之爲前後綴(如在a b c a b中,前綴和後綴均爲”ab”。)。當沒有任何相同的前後綴的情況下,next值爲0。否則next值爲k。k可以理解爲子串的長度。
  
  再舉個例子來看一下設麼是前綴和後綴:

字符串:”test”
前綴爲: t, te, tes
後綴爲:est, st, t

  next值舉例:

P  a   b  a   b  a  
j   0   1  2   3   4
next -1   0  0   1  2

   我們來分析一下next值的產生過程:

“a”的前綴和後綴都爲空集,共有元素的長度爲0;
“ab”的前綴爲[a],後綴爲[b],共有元素的長度爲0;
“aba”的前綴爲[a, ab],後綴爲[ba, a],共有元素的長度0;
“abab”的前綴爲[a, ab, aba],後綴爲[bab, ba, b],共有元素的長度爲0;
“ababa”的前綴爲[a, ab, aba, abab],後綴爲[baba, aba, ba, a],共有元素爲”a”,長度爲1;

   由next數組的定義可知,我們可以遞推出如下結論:

(1) next[0] = -1
假設next[j]=k, 即P[0, k-1]==P[j-k, j-1]
(2) 若P[j] == P[k],則有P[0, k]==P[j-k, j],很顯然,next[j+1]=next[j]+1=k+1;
(3) 若P[j] != P[k],則可以把其看做模式匹配的問題,即匹配失敗的時候,k值如何移動,顯然k=next[k]。

  那麼具體的實現可以是(JAVA):

    static void getNext(char[] p){
        int[] next = new int[p.length];
        next[0] = -1;

        int j = 0;
        int k = - 1;
        while(j < p.length - 1){
            // 匹配的情況下,p[j]==p[k]
            if (k == -1 || p[j] == p[k]) {
                j++;
                k++;
                next[j] = k;
            } else{
                //p[j]!=p[k]
                k = next[k];
            } 
        }
    }

  或者可以使用直接求解(java):

    static void getNext(char[] p) {
        int i, j, temp;
        int[] next = new int[p.length];
        for (i = 0; i < p.length; i++) {
            if (i == 0) {
                next[i] = -1; // next[0]=-1
            } else if (i == 1) {
                next[i] = 0; // next[1]=0
            } else {
                temp = i - 1;
                for (j = temp; j > 0; j--) {
                    if (equals(p, i, j)) {
                        next[i] = j; // 找到最大的k值
                        break;
                    }
                }
                if (j == 0)
                    next[i] = 0;
            }
        }
    }

    // 判斷p[0...j-1]與p[i-j...i-1]是否相等
    static boolean equals(char[] p, int i, int j) {
        int k = 0;
        int s = i - j;
        for (; k <= j - 1 && s <= i - 1; k++, s++) {
            if (p[k] != p[s])
                return false;
        }
        return true;
    }

  KMP算法的思想就是:我們的原則還是從左向右匹配, 在匹配過程稱,若發生不匹配的情況,如果next[j]>=0,則目標串的指針i不變,將模式串的指針j移動到next[j]的位置繼續進行匹配;若next[j]=-1,則將i右移1位,並將j置0,繼續進行比較。
  
  KMP的匹配過程:

搜索詞:      abcdabd
匹配值(next):0000120

1、字符串和模式串(搜索詞)對齊
bbc abcaab abcdabcdabdec
abcdabd

2、字符串的第一個字符爲b與搜索詞的第一個字符a不匹配。搜索詞後移一位
bbc abcaab abcdabcdabdec
 abcdabd

3、因爲b與a不匹配,搜索詞再往後移。
bbc abcaab abcdabcdabdec
  abcdabd

4、就這樣,直到字符串有一個字符,與搜索詞的第一個字符相同爲止。
bbc abcaab abcdabcdabdec
    abcdabd

5、接着比較字符串和搜索詞的下一個字符,還是相同, 直到字符串有一個字符,與搜索詞對應的字符不相同爲止。
即搜索詞的d與字符串的空格不符。這時,最自然的反應是,將搜索詞整個後移一位,再從頭逐個比較。
這樣做雖然可行,但是效率很差,因爲你要把"搜索位置"移到已經比較過的位置,重比一遍。
bbc abcaab abcdabcdabdec
     abcdabd

6、一個基本事實是,當空格與d不匹配時,你其實知道前面六個字符是"abcdab"。
KMP算法的想法是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。
查表可知,最後一個匹配字符b對應的"部分匹配值"2,因此按照下面的公式算出向後移動的位數: 
移動位數 = 已匹配的字符數 - 對應的部分匹配值, 即 6 - 2 等於4,所以將搜索詞向後移動4位。
bbc abcaab abcdabcdabdec
        abcdabd

7、因爲空格與c不匹配,搜索詞還要繼續往後移。這時,已匹配的字符數爲2"ab"),對應的"部分匹配值"0。
所以,移動位數 = 2 - 0,結果爲 2,於是將搜索詞向後移2位。
bbc abcaab abcdabcdabdec
          abcdabd    

8、因爲空格與a不匹配,繼續後移一位。           
bbc abcaab abcdabcdabdec
           abcdabd    

9、逐位比較,直到發現c與d不匹配。於是,移動位數 = 6 - 2,繼續將搜索詞向後移動4位。
bbc abcaab abcdabcdabdec
               abcdabd     

10、逐位比較,直到搜索詞的最後一位,發現完全匹配,於是搜索完成。
如果還要繼續搜索(即找出全部匹配),移動位數 = 7 - 0,再將搜索詞向後移動7位,這裏就不再重複了。                         

  代碼實現如下(java):

    static int KMPMatch(char[] T, char[] p) {
        int i = 0;
        int j = 0;
        int next[] = getNext(p);## 標題 ##
        while (i < T.length) {
            if (j == -1 || T[i] == p[j]) {
                i++;
                j++;
            } else {
                j = next[j]; // 消除了指針i的回溯
            }
            if (j == p.length)
                return i - p.length;
        }
        return -1;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章