匹配字串之——KMP演算法深入理解

apkcore發表於2018-11-20

KMP演算法的用途

一般在字串匹配的時候,我們通常想到的就是使用KMP演算法來處理。

KMP的使用,在網上有很多例項,但是講得很讓人容易接受的不是很多,但總感覺那層窗戶紙沒有捅破,這裡我結合前人的知識,講下自己的理解,希望大家多指教。

常規暴力匹配法

有一個字串"BBC ABCDAB ABCDABCDABDE",我想知道,裡面是否包含另一個字串"ABCDABD"? 預處理時間 O(0) 時間複雜度為0(N*M)

/**
     * 暴力匹配法
     *
     * @param text 原字串
     * @param text2 要匹配的串
     * @return
     */
    private static int violentMatch(String text, String text2) {
        int i = 0;
        int j = 0;
        while (i < text.length() && j < text2.length()) {
            if (text.charAt(i) == text2.charAt(j)) {
                i++;
                j++;
            } else {
                i = i - j + 1;
                j = 0;
            }
        }
        if (j == text2.length()) {
            return i - j;
        } else {
            return -1;
        }
    }
複製程式碼

使用KMP演算法

如題:有一個字串"BBC ABCDAB ABCDABCDABDE",我想知道,裡面是否包含另一個字串"ABCDABD"?

  1. 字串BBC ABCDAB ABCDABCDABDE的第一個字元與搜尋字串的第一個字元進行比較,因為B與A不匹配,所以搜尋詞後移一位。
    在這裡插入圖片描述
    在這裡插入圖片描述
  2. 因為B與A不匹配,搜尋詞再往後移。
  3. 就這樣,直到字串有一個字元,與搜尋詞的第一個字元相同為止。
    在這裡插入圖片描述
  4. 接著比較字串和搜尋詞的下一個字元,還是相同。
  5. 直到字串有一個字元,與搜尋詞對應的字元不相同為止。
    在這裡插入圖片描述
  6. 這時,最自然的反應是,將搜尋詞整個後移一位,再從頭逐個比較。這就是上面所展示的暴力匹配法,這樣做雖然可行,但是效率很差,因為你要把"搜尋位置"移到已經比較過的位置,重比一遍。
  7. 一個基本事實是,當空格與D不匹配時,你其實知道前面六個字元是"ABCDAB"。KMP演算法的想法是,設法利用這個已知資訊,不要把"搜尋位置"移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。
  8. 怎麼做到這一點呢?可以針對搜尋詞,算出一張《部分匹配表》。這張表是如何產生的,後面再說,這裡只要先用就可以了。
  9. 已知空格與D不匹配時,前面六個字元"ABCDAB"是匹配的。查表可知,最後一個匹配字元B對應的"部分匹配值"為2,因此按照下面的公式算出向後移動的位數: 移動位數 = 已匹配的字元數 - 對應的部分匹配值 因為 6 - 2 等於4,所以將搜尋詞向後移動4位。
    在這裡插入圖片描述
  10. 因為空格與C不匹配,搜尋詞還要繼續往後移。這時,已匹配的字元數為2("AB"),對應的"部分匹配值"為0。所以,移動位數 = 2 - 0,結果為 2,於是將搜尋詞向後移2位。
    在這裡插入圖片描述
  11. 因為空格與A不匹配,繼續後移一位。
    在這裡插入圖片描述
  12. 逐位比較,直到發現C與D不匹配。於是,移動位數 = 6 - 2,繼續將搜尋詞向後移動4位。
  13. 逐位比較,直到搜尋詞的最後一位,發現完全匹配,於是搜尋完成。如果還要繼續搜尋(即找出全部匹配),移動位數 = 7 - 0,再將搜尋詞向後移動7位,這裡就不再重複了。
    在這裡插入圖片描述

部分匹配表的生成

首先,要了解兩個概念:"字首"和"字尾"。 "字首"指除了最後一個字元以外,一個字串的全部頭部組合;"字尾"指除了第一個字元以外,一個字串的全部尾部組合。

在這裡插入圖片描述
即:
在這裡插入圖片描述
在這種情況下,向右移動位數也可以這樣表示:已匹配字元6-上一個字元B對應的最大長度2=4,向右移動4位。 由此可得:

失配時,模式串向右移動的位數為:已匹配字元數-失配字元的上一位字元所對應的最大長度值。

next陣列的求解思路

    /**
     * 求next陣列
     * @param dest
     * @return
     */
    private static int[] kmpNext(String dest) {
        int[] next = new int[dest.length()];
        next[0] = 0;//模板字串的第一個字元最大前字尾長度固定為0
        //for迴圈從第二個字元開始,依次計算每一個字元對應的next值
        for (int i = 1, j = 0; i < dest.length(); i++) {
            //重點,難點:遞迴求最大的相同的前字尾長度
            while (j > 0 && dest.charAt(j) != dest.charAt(i)) {
                j = next[j - 1];
            }
            //如果相等,那麼最大相同前字尾長度加1
            if (dest.charAt(i) == dest.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }
複製程式碼
  1. 已知前一步計算時最大相同的前字尾長度為j(j>0),即dest[0]到dest[j-1];
  2. 此時比較搜尋詞第j項與上面的字串比較
  3. 如果相等,那麼很簡單跳出while迴圈;
  4. 如果不相等,那麼只要求dest[j-1]這個位置上,即next[j-1]來求最大的相同前字尾。

KMP的整體時間複雜度為O(m + n) 附程式碼如下

class KmpTest {
    public static void main(String[] args) {
        String text = "BBC ABCDAB ABCDABCDABDE";
        String text2 = "ABCDABD";
//        System.out.println(violentMatch(text, text2));
        int[] ints = kmpNext(text2);
        int res = kmp(text, text2, ints);
        System.out.println(res);
        System.out.println("==================================");
        for (int anInt : ints) {
            System.out.println(anInt);
        }
    }

    /**
     * 暴力匹配法
     *
     * @param text
     * @param text2
     * @return
     */
    private static int violentMatch(String text, String text2) {
        int i = 0;
        int j = 0;
        while (i < text.length() && j < text2.length()) {
            if (text.charAt(i) == text2.charAt(j)) {
                i++;
                j++;
            } else {
                i = i - j + 1;
                j = 0;
            }
        }
        if (j == text2.length()) {
            return i - j;
        } else {
            return -1;
        }
    }

    /**
     * 求next陣列
     *
     * @param dest
     * @return
     */
    private static int[] kmpNext(String dest) {
        int[] next = new int[dest.length()];
        next[0] = 0;//模板字串的第一個字元最大前字尾長度固定為0
        //for迴圈從第二個字元開始,依次計算每一個字元對應的next值
        for (int i = 1, j = 0; i < dest.length(); i++) {
            //遞迴求為最大的相同的前字尾長度
            while (j > 0 && dest.charAt(j) != dest.charAt(i)) {
                j = next[j - 1];
            }
            //如果相等,那麼最大相同前字尾長度加1
            if (dest.charAt(i) == dest.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }

    /**
     * KMP演算法
     *
     * @param str  文字串
     * @param dest 模式串
     * @param next
     * @return
     */
    private static int kmp(String str, String dest, int[] next) {
        for (int i = 0, j = 0; i < str.length(); i++) {
            while (j > 0 && str.charAt(i) != dest.charAt(j)) {
                j = next[j - 1];
            }
            if (str.charAt(i) == dest.charAt(j)) {
                j++;
            }
            if (j == dest.length()) {
                return i - j + 1;
            }
        }
        return 0;
    }
}

複製程式碼

從頭到尾徹底理解KMP

【經典演算法】——KMP,深入講解next陣列的求解

下面是我的公眾號,歡迎大家關注我

匹配字串之——KMP演算法深入理解

相關文章