KMP算法(一):正常邏輯求解(next數組)

一、KMP是什麼

KMP算法是爲了解決字符串匹配效率而提出的,提出者爲D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位大牛,故稱爲“KMP”算法。

二、暴力求解算法

1、題目:假設一個父字符串是father,子字符串是son,在father中查找son,如果存在則返回son在father中的起始索引,不存在則返回-1。

2、最簡單的解法就是使用循環,挨個字符比較,如果匹配就繼續,如果不匹配則father和son同時回退前進的長度,重新計算。java代碼如下:

/**
 * 暴力算法
 */
private static int violentMatch(String father, String son) {
    int fatherLength = father.length();
    int sonLength = son.length();
    //i記錄在father中的位置
    int i = 0;
    //i記錄在son中的位置
    int j = 0;
    while (i < fatherLength && j < sonLength) {
        if (father.charAt(i) == son.charAt(j)) {
            //如果父子當前的字符匹配成功,則同時前進以爲,匹配下一個
            i++;
            j++;
        } else {
            //匹配不成功,則回退前進的長度
            //因爲j每次都是從son的第一位開始,也就是0,所以i-j就是回到這一輪開始的位置,再+1則表示下一個字符(當前輪開始的字符匹配不成功)
            i = i - j + 1;
            j = 0;
        }
    }
    //如果跳出循環之後,j等於子串的長度(循環進行的條件之一是j < sonLength,匹配成功後j++ 就等於sonLength了)
    if (j == sonLength) {
        //此時i所在位置是son在father中的最後一個字符的後一位,所以起始位置要減去j,就是son在father中的第一位
        return i - j;
    }
    //沒有匹配成功就返回-1
    return -1;
}

3、暴力求解的問題在哪呢?

在於i跟j同步回溯,會造成很多不必要的對比次數。比如father=“abcdef", son="abx",

 

第一次開始比較的時候,到father的 c 位置就發現不匹配,結束回退再次循環,但是其實人工來看完全不需要從 b 和 c 再開始了,因爲第一次比較已經知道了father 的 b 和 c 與son的 a 是不一樣的,更何況son的 x 在father的前三位壓根就沒有,完全可以從 d 開始。

三、KMP算法之正常邏輯求解(next數組)

(一)、基本算法流程

假設father匹配到 i 位置,son匹配到 j 位置。

-如果j = -1, 或者當前匹配字符成功(father.charAt(i) == son.charAt(j)), 都令i++, j++, 繼續匹配下一個字符。

-如果j != -1, 且當前字符匹配失敗,則令 i 不變,j=next[j], 意思是當前字符匹配失敗,son相對於father向右移動了j - next[j] 位。就比如father="abcabcd", son="abcd", next[]=[-1, 0, 0, 0 ],匹配到father.charAt(3)時,father的字符是a,son的字符是d,i = 3, j = 3,這個時候如果暴力解法,則i = 1, j = 0,而KMP算法則是將 i 不變, j = next[j] = 0,意味着將son右移 j - next[j] = 3。

用代碼表示如下:

/**
 * KMP算法
 */
private static int match(String father, String son) {
    //通過子串計算next數組
    int[] next = getNext(son);
    int i = 0, j = 0;
    //一直循環到i等於father長度,或者j等於son長度
    while (i <= father.length() - 1 && j <= son.length() - 1) {
        //-1表示son需要從頭匹配,||後面的語句表示字符相等,後移一位
        if (j == -1 || father.charAt(i) == son.charAt(j)) {
            i++;
            j++;
        } else {
            //字符不匹配,i不變,j回退到next[j]進行比較,或者說son右移j - next[j]位
            j = next[j];
        }
        if (j > son.length() - 1) {
            //匹配成功
            return i - son.length();
        }
    }
    return -1;
}

(二)、next數組計算原理

1、尋找前綴後綴最長公共元素長度(這裏看到一篇博客說的比較詳細,直接引用了,鏈接:https://blog.csdn.net/v_july_v/article/details/7041827

2、然後自己再解釋一下,爲什麼“next 數組考慮的是除當前字符外的最長相同前綴後綴”,見下圖

(三)、next計算代碼

private static int[] getNext(String son) {
    //因爲是自己跟自己比較,所以起始狀態j 比 i小1。只是-1不存在,所以下方next[0]設置成了-1
    //配合判斷條件j == -1,造成的結果就是i和j一起進一位。如果之前的步驟兩個字符相同,那就是都進一位繼續比較。
    // 如果不同,j置爲-1然後自增變成0,i進一位,就開始了多一位字符的運算。
    int i = 0, j = -1;
    int[] next = new int[son.length()];
    next[0] = -1;
    while (i < son.length() - 1) {
        //以son = "ababd"爲例
        //起始的時候j=-1,令i = 1, j = 0,所以next[1] = 0,而且不管是什麼字符串,都會是0,
        // 因爲next 數組考慮的是除當前字符外的最長相同前綴後綴。所以i = 1的時候,next數組考慮的只有son[0]這一個字符
        //而當i=1了,j=0,son[1]和son[0]不相等,則j就要回退,回退到next[j],此處原理與match時候的j = next[j]的原理很相似。
        if (j == -1 || son.charAt(i) == son.charAt(j)) {
            i++;
            j++;
            next[i] = j;
        } else {
            //son[i]和son[j]不相等,則j就要回退,回退到next[j],此處原理與match時候的j = next[j]的原理很相似。
            // 回退到j = 0時就相當於此時的字符與son[0]開始比較,不相等則j=next[0]=-1,緊接着就各自加1,開始多一位字符長度的比較
            j = next[j];
        }
    }
    //感興趣的可以再用son="abcdabcdcab"等進行debug觀察計算和回溯過程
    return next;
}

 

四、自述

這周開始在看《大話數據結構》,這算是書中目前爲止遇到的第一個真正意義上的算法吧,就把我難住了,大概看了一天半吧纔有了大概的輪廓,但是感覺細節還不是很清晰,就想着通過寫博客的方式來講述,加深理解。如果大家覺得哪裏講的有問題或者不夠透測,可以留言討論。

邊寫博客邊加深理解,也算是耗費了一些時間,一週就這樣過去了(階段性空閒,程序員都懂的,可別說我工作不飽和好吧……^_^)。本來想再把知乎上看到的另一種解法--確定有限狀態機 也寫在這裏的,但是時間來不及了,畢竟還是要工作的。就準備等週末來寫好了。然後下週再研究KMP算法的改進算法--nextval數組方式,以及Sunday算法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章