第五章 字串專題 ---------------- 字串匹配(二)----KMP演算法

Curtis_發表於2019-03-19

什麼是KMP演算法:

  KMP演算法是一種改進的字串匹配演算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,因此人們稱它為克努特——莫里斯——普拉特操作(簡稱KMP演算法)。KMP演算法的關鍵是利用匹配失敗後的資訊,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是實現一個next()函式,函式本身包含了模式串的區域性匹配資訊。時間複雜度O(m+n)。

先來看看暴力解法:

  假設主串是目標字串為S,模式串是待匹配的字串為P。用暴力演算法匹配字串過程中,我們會把S[0] 跟 P[0] 匹配,如果相同則匹配下一個字元,直到出現不相同的情況,此時我們會丟棄前面的匹配資訊,然後把S[1] 跟 P[0]匹配,迴圈進行,直到主串結束,或者出現匹配成功的情況。這種丟棄前面的匹配資訊的方法,極大地降低了匹配效率。時間複雜度O(m*n)

程式碼:

/**
     * 暴力解法
     * @param s 主串
     * @param p 模式串
     * @return
     */
    private static int indexOf(String s, String p) {
        int i = 0;
        int sc = i;
        int j = 0;
        while(sc<s.length()){
            if (s.charAt(sc)==p.charAt(j)) {
                sc++;
                j++;
                if (j==p.length()) {
                    return i;
                }
            }else {
                i++;
                sc=i; // 掃描指標以i為起點
                j=0;  // 恢復為0
            }
        }
        return -1;
    }

       而在KMP演算法中,對於每一個模式串我們會事先計算出模式串的內部匹配資訊,在匹配失敗時最大的移動模式串,以減少匹配次數。這樣主串的指標就不會回溯了,就能保證一次主串的迴圈就能解決問題。比如,在簡單的一次匹配失敗後,我們會想將模式串儘量的右移和主串進行匹配。右移的距離在KMP演算法中是如此計算的:在已經匹配的模式串子串中,找出最長的相同的字首和字尾,然後移動使它們重疊。

      這裡可以看出指標指向的地方匹配失敗,而在已經匹配的模式串子串"ABCAB"中,最長的相同的字首和字尾是"AB",長度為2,所以j要向右移動到位置2,因為有相同的字首和字尾,那麼在移動的過程中,這幾個字元肯定是能夠匹配成功的,就不用去比較了。由此可以得出結論:當匹配失敗時,在已經匹配的模式串子串中,如果最前面的k個字元和j之前的最後k個字元是一樣的,那麼j要移動到下一個位置k。

       然而,如果每次都要計算最長的相同的字首反而會浪費時間,所以對於模式串來說,我們會提前計算出每個匹配失敗的位置應該移動的距離,花費的時間就成了常數時間。因為在P的每一個位置都可能發生不匹配,也就是說我們要計算每一個位置j對應的k,所以用一個陣列next來儲存,next[j] = k,表示當S[i] != P[j]時,j指標的下一個位置k。那到底怎麼計算next陣列呢?

  當j為0時,如果這時候不匹配,這種情況,j已經在最左邊了,不可能再移動了next[0] = -1;那麼當j為1的時候,如果不匹配,j指標一定是後移到0位置的,因為它前面也就只有這一個位置了,next[1] = 0;如果p[j]==p[k]或者k<0,next[++j] = ++k,否則,k=next[k]。

 

程式碼: 

public static int[] next(String ps) {
    int pLength = ps.length();
    int[] next = new int[pLength + 1];
    char[] p = ps.toCharArray();
    next[0] = -1;
    if (ps.length() == 1)
      return next;
    next[1] = 0;

    int j = 1;
    int k = next[j]; //看看位置j的最長匹配字首在哪裡

    while (j < pLength) {
      //現在要推出next[j+1],檢查j和k位置上的關係即可
      if (k < 0 || p[j] == p[k]) {
        next[++j] = ++k;
      } else {
        k = next[k];
      }
    }
    return next;
  }

那麼完整的程式碼就是:

public class KMP {

    public static void main(String[] args) {
        String src = "babababcbabababb";
        int index = indexOf(src, "bababb");
        System.out.println("暴力破解法:"+index);
        index = indexOf1(src, "bababb");
        System.out.println("KMP演算法:"+index);
    }

    //O(m+n),求count 總共出現了多少次
    private static int indexOf1(String s, String p) {
        if (s.length()==0||p.length()==0) {
            return -1;
        }
        if (p.length()>s.length()) {
            return -1;
        }
        
//        int count = 0;
        int []next = next(p);
        int i = 0;//s位置
        int j = 0;//p位置
        int sLen = s.length();
        int pLen = p.length();
        
        while(i<sLen){
            // ①如果j = -1,或者當前字元匹配成功(即S[i] == P[j]),都令i++,j++
            // j=-1,因為next[0]=-1,說明p的第一位和i這個位置無法匹配,這時i,j都增加1,i移位,j從0開始
            if (j == -1 || s.charAt(i) == p.charAt(j)) {
                i++;
                j++;
            } else {
                // ②如果j != -1,且當前字元匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]
                // next[j]即為j所對應的next值
                j = next[j];
            }
            if (j == pLen) {// 匹配成功了
//                count++;
//                j = next[j]; 
                // 上面兩行程式碼是計數模式字串總共出現了多少次的
                return (i - j);
            }
        }
        return -1;
//        return count; // -1
    }

    public static int[] next(String ps){
        int pLength = ps.length();
        int []next = new int[pLength+1];
        char []p = ps.toCharArray();
        next[0] = -1;
        if (ps.length()==1) {
            return next;
        }
        next[1] = 0;
        
        int j = 1;
        int k = next[j];  // 看看位置j的最長匹配字首在哪裡
        
        while(j<pLength){
            // 現在要推出next[j+1],檢查j和k位置上的關係即可
            if (k<0||p[j]==p[k]) {
                next[++j] = ++k;
            }else {
                k = next[k];
            }
        }
        return next;
    }
    /**
     * 暴力解法
     * @param s 主串
     * @param p 模式串
     * @return
     */
    private static int indexOf(String s, String p) {
        int i = 0;
        int sc = i;
        int j = 0;
        while(sc<s.length()){
            if (s.charAt(sc)==p.charAt(j)) {
                sc++;
                j++;
                if (j==p.length()) {
                    return i;
                }
            }else {
                i++;
                sc=i; // 掃描指標以i為起點
                j=0;  // 恢復為0
            }
        }
        return -1;
    }

}

結果:

 

 

相關文章