【原創】巧解KMP算法,循序漸進,看我是怎麼自己寫一個出來的

作者:DCTANT

如需轉載請說明出處!

KMP算法的關鍵在於如何省時間,怎麼減少重複匹配的次數。

這還不簡單,與其每個都傻乎乎的比較,不如直接就判斷個首字母,首字母不同還比什麼,直接跳過,也就等於matchIndex+1。

經過漫長的首字母匹配,終於找到首字母一樣的了,然後纔開始匹配,既然比都開始比了,總不能什麼成果都不留下吧,這也太浪費電腦資源了,當發現某個字符不同時,記錄下之前已經匹配完成多少個了,下次比較直接跳過這些個數不就行了!當然這會存在一些問題,我之後再說,先拿個最簡單字符串舉個例子:

主字符串:FFFDXTTTDCGDCT

匹配串:DCT

匹配開始,首先就去找D唄,發現D的index在3,那麼matchIndex就是3,且從index4的X開始匹配,因爲D已經知道一樣了,不需要在比較了。接着就是X和C的比較,發現這兩個不一樣了,怎麼辦呢?matchIndex向右移唄,移幾位呢?移一位唄(後面能夠優化,再說)matchIndex變爲4,D和X(index爲4)開始比較。

然後就又是繼續找D的過程了,發現index爲8的地方有個D,那麼匹配繼續開始了,C和C(index爲9)比較發現一樣,成果+1,T和G(index爲10)比較,發現不同了!那麼matchIndex應該向後移幾位呢?如果移1位,那就太辜負電腦做出的匹配嘗試了,明顯知道C和C都匹配上了,可能再與D進行匹配了,後移數量應該是1+成果數(如果這麼盲目移會導致問題,下面會說)。matchIndex+2,目前爲D和G(index爲10)開始比較。

然後就又是繼續找D的過程了,發現index爲11的地方有個D,那麼匹配繼續開始了,C和C(index爲12)比較發現一樣,成果+1,T和T(index爲13)比較發現一樣,成果再+1。匹配串遍歷完成,發現成果數量等於匹配串總長度-1,說明匹配成功!matchIndex即11。

完整圖,包括匹配後失敗的第一次匹配:

省略匹配失敗後的第一次匹配,圖爲(這樣看起來更簡潔):

如果以上理解了,那麼恭喜你,KMP算法基本上懂了80%了。但是這麼盲目移動會導致一個問題——移過頭了!!

就拿KMP算法網上最經典的例子來說:

主字符串:BBC ABCDAB ABCDABCDABDE

匹配串:ABCDABD

首先最簡單的,找A唄!注意:BBC和ABCD之間有個空格,ABCDAB和ABCDABCDABDE之間也有個空格。

首先找到A在index爲4的位置,matchIndex等於4,然後比較開始,B和B(index爲5)比,成果+1,C和C(index爲6)比,成果+1,一直比到D和空格(index爲10),發現不同了,按照之前的做法,matchIndex下一次比較的位置爲1+成果數,這裏的成果數爲5,即BCDAB五個字符是相同的,matchIndex爲10(4+1+5),即A開始和空格(index爲10)開始比較。

然後就又是繼續找A的過程了,發現index爲11的地方有個A,那麼匹配繼續開始了,重複上述過程,發現C(index爲17)和D不同,成果數爲5,即BCDAB五個字符,matchIndex爲17(11+1+5),即A和C(index爲17)開始比。

然後就又是繼續找A的過程了,發現index爲19的地方有個A,那麼匹配繼續開始了,結果發現主串剩餘長度還沒匹配串的長,說明匹配失敗了!!這樣問題不就大了,很明顯代碼寫錯了!!問題的根源就在於移過頭了,matchIndex移太快了,完全沒有考慮到被匹配串中(ABCDABD)的也有和匹配串頭字符(A)一樣的元素,即有兩個A!!問題找到了,那麼就很好解決了。

完整圖爲,包括匹配後失敗的第一次匹配:

省略匹配失敗後的第一次匹配,圖爲(這樣看起來更簡潔):

如果(if)被配匹配的字符串(假設爲ABCDBBD)中僅包含一個匹配字符串(ABCDABD)的首字符(A),則按照原來的做法,移動最大長度即1+成果數

如果(else)被配匹配的字符串(假設爲ABCDABC)中包含多個(兩個A)匹配字符串的首字符(A),那麼移動的最大長度爲這個第二個首字符出現的位置,即1+第二個首字符的index,上面的例子爲5,並非成果數。那麼問題就解決了,圖片演變爲:

問題完美解決了,KMP算法也就誕生了,其實過程非常之簡單,結果這東西在網上被描繪成洪水猛獸一般,讓人難以理解。我在大學上課就沒搞懂,書上反反覆覆看了幾遍也沒看懂,網上教程翻了又翻,也都說的不是人話。結果昨天失眠,晚上隨便想想,突然就想明白了!其實真的非常簡單,哪有什麼難的啊。只是老師不會教,書上不會寫罷了。

當然這裏還有一個稍微可以再優化那麼一點點的地方,就是如果匹配串失敗後最後一個字符和首字符如果不同,例如示例1中的FFFDXT和DCT中的C不匹配,且X和D並不相同,移動的數量應該爲1還能再加1,下一次匹配完全可以是D和T匹配,而不是D再和X去匹配,本質其實是相同的,即使有多移這麼一位,性能變化也是微乎其微。

然後就是上代碼,代碼去掉註釋後,真的很短啊!

代碼註釋非常詳細,相信你們都能看懂:

public class StringMatcher {
    /**
     * 字符串匹配方法
     *
     * @param totalString 字符串主串
     * @param matchString 匹配串
     * @return
     */
    public int matcher(String totalString, String matchString) {
        int matchIndex = 0; // INFO: DCTANT: 2019/9/8 匹配起始點 
        char[] matchCharArray = matchString.toCharArray();
        char[] totalCharArray = totalString.toCharArray();
        char matchHeadChar = matchCharArray[0]; // INFO: DCTANT: 2019/9/8 匹配串首字符 

        while (true) {
            if (matchIndex + matchCharArray.length - 1 > totalCharArray.length) {
                // INFO: DCTANT: 2019/9/8 防止下標越界,標記匹配結束
                break;
            }
            char charInString = totalCharArray[matchIndex];
            if (charInString == matchHeadChar) {
                // INFO: DCTANT: 2019/9/8 首字符相等纔會開始匹配工作
                int continueTimes = 1; // INFO: DCTANT: 2019/9/8 總共連續次數(成果數),最少都得移1次
                int sameHeadCharIndex = -1; // INFO: DCTANT: 2019/9/8 被匹配的字符串中是否存在和匹配串首字符相同的字符
                for (int i = 1; i < matchCharArray.length; i++) { // INFO: DCTANT: 2019/9/8 i爲1,匹配串去頭開始比較比較,因爲能進這個if,說明首字符已經相同了
                    char matchStringChar = matchCharArray[i];
                    char totalStringChar = totalCharArray[matchIndex + i];
                    if (totalStringChar == matchHeadChar && sameHeadCharIndex == -1) {
                        // INFO: DCTANT: 2019/9/8 查找被匹配的字符串中是否存在和首字符相同的值 
                        sameHeadCharIndex = continueTimes;
                    }
                    if (matchStringChar == totalStringChar) {
                        // INFO: DCTANT: 2019/9/8 如果相同則繼續匹配,成果數+1
                        continueTimes++;
                    } else {
                        // INFO: DCTANT: 2019/9/8 如果不同則中斷匹配
                        break;
                    }
                }

                if (continueTimes == matchCharArray.length) {
                    // INFO: DCTANT: 2019/9/8 匹配成果次數等於需要匹配的字符串長度減一(因爲已經去頭),則說明匹配成功了
                    return matchIndex;
                } else {
                    // INFO: DCTANT: 2019/9/8 如果匹配失敗,則改變下一次匹配的起點 
                    if (sameHeadCharIndex != -1) {
                        // INFO: DCTANT: 2019/9/8 如果被匹配的字符串中存在和匹配串首字符相同的字符,則移動到下個首字符開始的地方
                        matchIndex += sameHeadCharIndex;
                    } else {
                        // INFO: DCTANT: 2019/9/8 如果去頭的匹配串中沒有和匹配串首字符相同的字符,則移動1+成果數
                        matchIndex += continueTimes + 1;
                    }
                }
            } else {
                // INFO: DCTANT: 2019/9/8 首字符都無法匹配,直接往後移匹配起始點 
                matchIndex++;
            }
        }
        return -1;
    }

最後是無聊的性能比較,這裏當然要和Java原生的indexOf比,這裏有個很奇怪的問題,我把indexOf這個方法從String類中拷出來後,比原方法的性能差了有近1倍,明明是一模一樣的代碼,我也不清楚是爲什麼,希望有大神能解釋一下。

結果是性能和Java原生的indexOf差不多,有時性能比它要好,有時又稍微差一點,反正五五開的水平,說明寫的沒什麼問題。

 

 

 

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