KMP算法基礎版和進階版簡單解釋-Java版

KMP算法

比較口語化的寫了爲什麼k = next[k]和改進版中next[j] = next[k],以及代碼理解中的注意點。

KMP算法是一種字符串匹配算法,其關鍵是利用匹配失敗後的信息,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。
最基礎的字符串匹配就是每一次匹配,模式串都重頭開始,主串後移一位,這樣時間複雜度爲O(m×n),而KMP是字符串匹配算法的改進,改進後的時間複雜度可以縮小至O(m+n)。

KMP算法基礎版

KMP算法的核心就是求模式串的next數組,簡單解釋一下,next[i]就是模式串除去第i個字符,從頭到第(i-1)個字符前綴與後綴最長重複的個數。

這裏要理解一下什麼是前綴和後綴,這個概念很重要!在“aba”中,前綴就是“ab”,即除去最後一個字符的剩餘字符串;後綴就是"ba",即除去第一個字符的後面全部的字符串。"aba"的前綴與後綴最長重複的個數就爲1。 特別要注意的是:前綴必須要從頭開始算,後綴要從最後一個字符開始算,中間截一段相同字符串是不行的!!!!

當字符串匹配時,如下圖:
字符串匹配
當匹配到C和D時,失配了。這個時候,模式串就應該往後移兩位,即下圖:
字符串匹配
爲什麼只往後移兩位呢,因爲第一位的A和第三位的A匹配了,此時就不需要重複匹配了,這就是KMP的精髓。那怎麼知道當主串模式串失配時,模式串往後移幾位,也就是重新定位到模式串的第幾個字符呢?
以上面的圖爲例,雖然ABAD失配,但是ABA是匹配的,也就是說主串的ABA與模式串的ABA完全匹配,除去當前的完全匹配,把模式串的ABA往後拖,兩個ABA的最大重複子字符個數。這就相當於主串是後綴,模式串是前綴,找出前綴與後綴最大的重複字符個數,在上圖中,ABA的前綴與後綴最大重複個數爲1,即模式串重新定位到1的位置(因爲字符串從0開始,所以這裏就省去了+1),讓B和C開始比較。所以模式串到i位失配時,下一個移動點就是模式串(0…i-1)位的前綴與後綴最大重複字符個數。

如何理解getNext中的k = next[k]

函數getNext(ps)就是算出next數組,next[i]就是模式串(0…i-1)位的前綴與後綴最大重複字符個數。這個就相當於模式串自己與自己匹配
注意要初始化next[0]=-1,也就是將模式串1作爲主串,模式串2作爲模式串開始匹配。模式串1的後綴與模式串2的前綴的最大重複字符個數,當模式串1遍歷到k與模式串2遍歷到j不匹配時,模式串2要重新定位,所以開始找模式串2即[0…k-1]中的前綴與後綴最大重複字符個數此時要知道,下面是重點!!!!!!!!!!!!!!
模式串2的[0…k-1] 不僅與模式串1的[j-k,j-1]是一樣的,與模式串1的[0…k-1]也是一樣的!!!(有點廢話,因爲模式串1和2本來就是同一個字符串,只不過錯位了。)模式串1的[0…k-1]的前綴與後綴最大重複字符個數爲next[k],而模式串2當前要移位,也就是當前的k=模式串2的[0…k-1]的前綴與後綴最大重複字符個數,模式串1=模式串2,所以k = next[k]!!!!!

KMP算法基礎版代碼

public int[] getNext(String ps) {
    char[] p = ps.toCharArray(); // 模式串
    int[] next = new int[p.length];
    next[0] = -1;// 初始化
    int j = 0; // 當前位置
    int k = -1; // 要返回的位置
    while (j < p.length - 1) {
        if (k == -1 || p[j] == p[k]) {
            // 當P[k] == P[j]時, 有next[j+1] == next[j] + 1
            next[++j] = ++k;
        } else {
            k = next[k];
        }
    }
    return next;
}

public int KMP(String ts, String ps) {
    char[] t = ts.toCharArray(); // 主串
    char[] p = ps.toCharArray(); // 模式串
    int i = 0; // 主串的位置
    int j = 0; // 模式串的位置
    // next[j]的值表示,當P[j] != T[i]時,j指針的下一步移動位置。
    int[] next = getNext(ps);

    while (i < t.length && j < p.length) {
        if (j == -1 || t[i] == p[j]) { // 當j爲-1時,移動i的同時j也要歸0
            i++;
            j++;
        } else
            j = next[j]; // j回到指定位置
    }
    // 返回模式串在主串中開始匹配的頭位置
    if (j == p.length) {
        return i - j;
    } else {
        return -1;
    }
}

KMP算法改進版

下面的圖摘自詳解KMP算法,這篇博客講的挺詳細,對KMP不瞭解的可以重頭看一遍。
詳解KMP
上面這個現象最主要的原因就是 P[j] == P[next[j]]。也就是p[j]失配時,下一步會跳到j = next[j],而p[next[j]]的值與p[j]的值一模一樣,就像上圖中B跳到了B。如何避免這個問題呢,需要將getNext中的核心代碼給改一下,如下圖:

代碼修改
一開始看這個代碼比較難理解,可以分解下來看。最核心的更改就是,左邊的next[++j] = ++k 變成了一個if…else…判斷。而右圖中的else其實算法內容和next[++j] = ++k是一樣的,也就是,右邊多了一個if (p[++j] == p[++k]) {next[j] = next[k]; }。最主要的是理解這句代碼,結合下圖更易理解。
圖例
當連續兩個(便於理解,假設爲兩個,實際上兩個及兩個以上)p[k]與p[j]匹配時,會形成上圖所畫的樣子,如果模式串1作爲模式串與主串匹配,若匹配到位置3的時候失配了,它下一步會跳到位置1繼續比較開始匹配(怎麼跳的看上面基礎版的)。但現在我們是要他跳到位置0,重頭開始匹配(跳到位置1沒意義)。

接下來,用代碼中的k和j重新敘述一遍上面的話!!!!!!!!!!
如果模式串1作爲模式串與主串匹配,若匹配到位置 j+1 的時候失配了,它下一步會跳到位置 k+1 繼續比較開始匹配(即 next[++j] = ++k;也就是右圖中的next[j] = k;)(怎麼跳的看上面基礎版的)。但現在我們是要他跳到位置k,重頭開始匹配(跳到位置k+1沒意義)。

下面一段話是理解的精髓!!!!!!!!!
因爲k+1這個位置匹配了的結果依舊是失配,所以就算跳到了k+1,依舊還要往前跳,所以直接提取從k+1往前跳的值,即next[k+1],所以當前j+1失配時位置跳的過程可以理解爲:

j+1 —> k+1 —> next[k+1]

所以next[j+1]保存的值爲next[k+1],在代碼中爲next[j] = next[k],因爲上面一行的判斷中++了。

KMP算法改進版代碼

public int[] getNext(String ps) {
    char[] p = ps.toCharArray(); // 模式串
    int[] next = new int[p.length];
    next[0] = -1;// 初始化
    int j = 0; // 當前位置
    int k = -1; // 要返回的位置
    while (j < p.length - 1) {
        if (k == -1 || p[j] == p[k]) {
            if (p[++j] == p[++k]) {
                // 當兩個字符相等時要跳過
                next[j] = next[k];
            } else {
                next[j] = k;
            }
        } else {
            k = next[k];
        }
    }
    return next;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章