1. 程式人生 > 其它 >KMP演算法簡析

KMP演算法簡析

首先,KMP演算法是解決字串匹配問題的演算法,即在主串 S 中查詢子串 T。
  我們從問題入手,要在主串中查詢子串,顯然可以是用蠻力法逐個遍歷,即從主串的第一個字元開始和子串的第一個字元比較,若相等則繼續比較後續字元,若果不相等,則從主串的下一個的字元、子串的第一個字元重新開始比較。如果在主串遍歷完之後還沒有找到對應子串,則匹配失敗。
暴力法演算法描述如下:

 public static int findSubString(String s,String t){
        int i = 0;
        int j = 0;
        // 標記主串中開始比較的字元下標
        int index = 0;         
        //如果先主串完了就匹配不成功,子串先完了匹配成功。兩者都會結束迴圈
        while (i<s.length() && j<t.length()){
            
            //這個位置相等,繼續下一個位置
            if (s.charAt(i) == t.charAt(j)) {   
                i++;
                j++;
            }
      //這個位置不相等,則從主串的下一個字元、子串的第一個字元重新開始比較
            else{                             
                index++;
                i = index;
                j = 0;
            }
        }
        //迴圈結束,判斷子串是否被匹配完了。是的話返回主串中第一個匹配的字元下標
        if (j == t.length()) return index;
        else return -1;
    }

  這樣的做法顯然效率低下,因為存在大量的回溯。比如我們要比較匹配兩個字串 S:"abcabcacb" , T: "abcac" 。

  從第一次比較,我們就可以輕而易舉的發現:第二次比較和第三次比較是完全沒有必要的。這就是 KMP 的出發點所在,那如何確定這一次比較是不是有意義的呢?我們發現這與子串本身的性質有關。比如這題中,子串中有第二個 a ,如果兩個a 之間的字元匹配成功了,但是子串沒有完全匹配(就如第一張圖所示),那麼下次匹配時就可以直接把子串的第一個 a 和 與 (父串中 與 (子串第二個a)匹配)的字元匹配,而不用再回溯至父串的第二個字元。簡言之,就是直接從第一張圖跳轉到第四張圖。

  我們總結下,想要滿足這樣的跳步匹配,子串需要滿足什麼樣的性質呢?顯然子串中要有重複的部分,可以是單個字元,也可以是一個字串。而且這個重複的部分必須是與第一個字母開始的字串重複的。我們要尋找的是與 首字元開頭的字串 重複的最長部分。比如:S:"ababaab" ,T:"ababc"

  這個子串中最大重複部分是 ab ,我們下一次匹配時不僅不對主串 S 進行回溯,也不從子串的子一個字元開始匹配,而是從子串下標為 2 的字元開始匹配。你可能已經發現了:最大重複的部分是 2 個字元,下一次匹配也是從子串下標為 2 的位置開始和父串後續部分匹配。

  那我們現在要做的就是尋找子串中那個最大的重複部分,準確的說是他的長度。KMP 演算法中使用一個 next 陣列來存放這個長度:next[i] 表示從子串開頭T[0] 到 T[i] 的最大重複部分的長度。初始next[0]值預設為-1。比如對子串"abab" : next[1]=0 , next[2]=a(有最大重複部分 a ),next[3]=2(有最大重複部分 ab)。
下面是獲得這個 next 陣列的值的演算法:

static void getNext(String t,int[] next){
        //初始化 next[0] = -1;
        next[0] = -1;
        /*
        * j 用來標識 next 的下標,next[j] 的含義如上面所說的 next[i]。
        *     它也表示對 子串中前 j 個字元尋找最大重複部分。
        *
        * len 表示最大重複部分的長度。它在迴圈中是遞減的數值,以保證能首先找到最大的重複部分就退出迴圈
        *     初始值是 j-1
        *
        * i 用來逐個比較字串內重複的部分。後面細說。
        * */
        int i,j,len;
        for (j = 1; j < t.length(); j++) {

            for (len = j-1 ; len > 0; len--){
                
                for (i = 0; i < len; i++) {
                    if (t.charAt(i) != t.charAt(j-len+i)) break;
                }

                if (i == len){
                    next[j] = len;
                    break;
                }
            }
            if (len < 1) next[j] = 0;
        }
    }

  我們重點來看一下最內層迴圈實現的效果。if 語句裡面比較的內容是:i 和 (j-len)+i 的位置的數值。當只有 i 變化時,if 語句比較的是 i 和與它固定間距(j-len)位置的值。舉個例子,如果這個子串有 4 個字元,第一次進行最裡面迴圈的時候,這個固定間距是 1(len 的初始值是 j-1),換句話說,他想試驗是否存在長度為 3 的最長重複部分。當 len-1 後,這個固定間距變成了 2,對於長度為 4 的字串來說,就是想找到是否存在長度為 2 的最大重複部分。這樣一來,就實現了前面所說的 尋找最大重複部分的功能。

  在第二層迴圈裡面 判斷 i==len 是為了斷定是否存在 len 長度的子串。

  最後,我們將這個函式呼叫起來:

static int KMP(String s,String t){
        int i = 0;
        int j = 0;
        int[] next = new int[100];
        getNext(t,next);
        while (i<s.length() && j<t.length()){
            
            //這個位置相等,繼續下一個位置
            if (s.charAt(i) == t.charAt(j)){
                i++;
                j++;
            }
            /*
            * 這個位置不相等,要找到下一次匹配時主串和字串開始比較的位置
            * 主串的位置就是上面 迴圈到不相等的 i
            * 子串的位置就是 next[i] 的位置。前面說過。
            */
            else{
                j = next[i];
                if (j == -1){
                    i++;
                    j++;
                }
            }
        }
        System.out.println(j);
        if(j == t.length()) return i-t.length();
        else return -1;
    }

  這裡求next陣列的方法用的是暴力法。但是子串的長度一般不是太長,時間消耗不多。過幾天更新更好的方法。
  如有錯誤,請指正,謝謝。