淺入淺出KMP算法

在看算法基礎書籍時,看到KMP算法的解釋是用的DFA(有限狀態自動機),看的我一臉懵逼。所以,就去網上搜索有沒有更容易理解的方式去實現KMP算法。

看了很多篇,感覺下面這篇博文講的比較清楚,但是也花了我挺長時間去看懂的。(好吧好吧,智商不足=_=)

KMP,深入講解next數組的求解

后面經過自己的思考總結,在這里記錄一下自己對KMP算法的理解和實現。


KMP算法的原理

關于KMP算法的原理,上面給出的鏈接里寫的很詳細了,這里簡要的說一下。

假如在字符串 "ABCDABEFABCDABDE" 要查找 "ABCDABD" 的話,當我們遍歷匹配到最后一個字符D的時候:

ABCDAB E FABCDABDE      
ABCDAB D

發現不匹配的時候,按照最容易想到的暴力算法,應該是往后移一位,再重新從頭開始進行比較:

A B C D A B E F ABCDABDE      
  A B C D A B D

顯然,這些一位一位的往后移的比較是沒有意義的,我們通過觀察就能知道,應該直接往后移4位,讓子字符串里的開頭兩個字符 AB 對齊原字符串,再從第三個字符 C 開始比較:

A B C D A B E F A B C DEABDE      
        A B C D A B D

所以重點是如何利用子字符串自己本身自帶的這些信息來幫助我們跳過一些不必要的比較。下面來分析一下 "ABCDABD" 這個字符串的特點。

  • 假如我們在原字符串查找 "ABCDABD" 的第1個字符 A 就發現不匹配,那不用說,直接往后移1位。

  • 假如匹配到了 "ABCDABD" 的第2個字符 AB 發現不匹配,那還是直接往后移1位。

  • 假如匹配到了 第3,4,5個字符 ABCDA 發現不匹配,也沒有可利用的條件,那還是直接往后移1位。

  • 當我們匹配到了 "ABCDABD" 的第6個字符 "ABCDAB" 的時候,發現不匹配,但是,前5個字符 "ABCDA" 是已經匹配成功了的,并且結尾的字符A與開頭的字符A重復了.顯然我們可以移動4位,讓開頭的字符A與結尾的字符A對齊,再比較后面的字符是否和原字符串匹配。如下所示:

原字符串:ABCDA X???????
子字符串:ABCDA B                //B與X不相等

結尾與開頭重復字符數量:1,移動4位變成

原字符串:ABCDA X ???????
子字符串:    A B CDA B          //是不是發現移動后,還是比較B和X是否相等?這里是不是可以改進?(現在請忽視)
  • 當我們匹配到了第7個字符 "ABCDABD" 的時候,發現不匹配,而前6個字符 "ABCDAB" 是已經匹配成功了的,這時我們可以還是移動4位,讓開頭的字符AB與結尾的字符AB對齊,再比較后面的字符C是否和原字符串后面的字符相匹配。如下所示:
原字符串:ABCDAB ???????
子字符串:ABCDAB D

結尾與開頭重復字符數量:2,移動4位變成:

原字符串:ABCDAB ? ??????
子字符串:    AB C DABD     //直接比較第3位的C是否和原字符串的?是否相等

說到這里,其實我們想要解決的問題就是:

在匹配失敗的時候,怎么根據已經匹配過的字符的信息來決定往后移動多少位再重新進行匹配?

所以,我們接下來要做的事就是將上面對"ABCDABD"子字符串進行分析的過程總結出一個規律來,這也是部分匹配表的由來。如下圖所示:

部分匹配值

部分匹配值也就是結尾字符與開頭字符相等的數量,比如"ABCDAB"部分匹配值就是2,"AB"是重復的。并且可以推斷出

移動位數 = 已匹配的字符數 - 對應的部分匹配值

將這些部分匹配值存到數組里,則變成了next數組。


next數組的求解思路

next數組的求解的關鍵思想在于:

利用前面的next值去求下一個next值

舉個栗子:

如果next[i-1]對應的字符串是"ABABCABAB",此時next[i-1] = 4,代表最后4個字符"ABAB"和前4個字符是重復的。

  1. 假如next[i]對應的字符串是"ABABCABABC",即最后一個字符"C"跟上一次匹配成功的字符"ABAB"的下一個字符"C"相等,則匹配值在原來的next值上+1,即 next[i] = next[i-1]+1

  2. 假如next[i]對應的字符串是"ABABCABABD",即最后一個字符"D"跟上一次匹配成功的字符"ABAB"的下一個字符"C"不相等,我們可以觀察出來匹配值next[i] = 0。那是不是意味著求next[i]的值只要看A[i]A[next[i-1]]是否相等就能得出next[i]的值是next[i-1]+1或者是0了呢?

  3. 假如next[i]對應的字符串是"ABABCABABA",最后一個字符"A"跟上一次匹配成功的字符"ABAB"的下一個字符"C"也不相等,但我們能觀察的出來匹配值next[i] = 3而不是0。這里面藏著什么貓膩呢?

實際上,next[i-1]里保存的是i-1位置的最長公共前綴后綴的長度,比如字符串ABABCABAB,最長公共前綴后綴長度為4,也就是ABAB。但AB也是它的公共前綴后綴,只不過不是最長的罷了。所以,在上述的情況3中,當最后一個字符A匹配不成功時,我們還可以搶救一下它,退而求其次。既然想找理想的最長的公共前綴后綴失敗,那就期望一下稍短一些的公共前綴后綴去匹配,那具體是去匹配多長的字符呢?

對于位置i-1而言,公共前綴后綴的長度依次為:next[i-1], next[next[i-1]-1], next[next[next[i-1]-1]-1]......

還是以ABABCABAB為例,next[8] = 4, 最長公共前綴后綴為ABABnext[next[8]-1] = next[3] = 2,次長公共前綴后綴為ABnext[next[3]-1] = next[1] = 0,說明最短的公共前綴后綴就是AB了,長度為2。

代碼如下:

 public static int[] getNext(String pattern) {
        int N = pattern.length();
        int next[] = new int[N + 1];
        next[1] = 0;//顯然字符串的第1個字符的最大前后綴長度為0
        int k =0;//最大公共前后綴長度
        for (int i = 1; i < N; i++) {
            while(k > 0 && pattern.charAt(i) != pattern.charAt(k))
                k = next[k-1];
           if(pattern.charAt(i) == pattern.charAt(k)){
               k++;
           }
            next[i] = k;
        }
        return next;
    }

上面代碼里可能最難理解的就是for循環里的那個while循環了。其實這個while循環就是
上面所述的去匹配公共前綴后綴的過程,如果最長next[i-1]長度的沒匹配到,就匹配稍短一點的next[next[i-1]-1],還沒匹配到,就匹配更短一點的next[next[next[i-1]-1]-1]......直到實在是找不到公共前綴后綴了,也就是長度為0的時候,就跳出循環了。


KMP算法實現

先直接貼代碼:

   /**
     * 在original字符串里查找子字符串find的位置
     * @param original 原始字符串
     * @param find 待匹配字符串
     * @return 查找成功則返回匹配的首字符索引位置,否則返回-1
     */
    public static int indexOf(String original, String find) {
        int next[] = getNext(find);
        int j = 0;
        for (int i = 0; i < original.length(); i++) {
            while (j > 0 && original.charAt(i) != find.charAt(j))
                j = next[j-1];
            if (original.charAt(i) == find.charAt(j))
                j++;
            if (j == find.length()) {
                return i - j + 1;
            }
        }
        return -1;
    }

上面代碼里可能最不容易理解的就是內部的while循環了:

while (j > 0 && original.charAt(i) != find.charAt(j))
                j = next[j];

其實這個過程就是在根據部分匹配值來移動子字符串find的比較位置,跟我們最開始分析KMP原理的步驟是一樣的。同樣的,我們還是來舉個栗子:

假如原字符串original是AACDABEAACDAADEF,待匹配的子字符串find是AACDAAD

在依次匹配字符的過程中,當i=5, j=5時,出現第一次不字符不匹配:

original.charAt(5) != find.charAt(5) //即 'B' != 'A'

AACDA B EAACDAADEF
AACDA A D

這時執行循環里的語句,j = next[j] = next[5] = 1; 這就意味著再次比較original.charAt(i) != find.charAt(j)的時候,變成了下面這樣:

AACDA B EAACDAADEF
    A A CDAAD   // j=1,find.charAt(j) = 'A'

這就意味著將子字符串往后移動了4位,即移動位數4 = 已匹配的字符數5 - 對應的部分匹配值1

好的,KMP算法就到此結束了。~(~ ̄▽ ̄)~

更多思考

在前面移位的時候,我們舉的栗子如下:

原字符串:ABCDA X???????
子字符串:ABCDA B                //B與X不相等

結尾與開頭重復字符數量:1,移動4位變成

原字符串:ABCDA X ???????
子字符串:    A B CDA B         //是不是發現移動后,還是比較B和X是否相等?

可能大家看到這個栗子的時候也有點奇怪,既然移動后,還是比較B和X,可我們在移動前就已經比較過了,是不相等的。所以這里是不是可以再往后多移2位?

也就是說這里不用匹配成功了的ABCDA的匹配值1,而是使用當前匹配失敗了的ABCDAB的匹配值2?當然了,更多細節問題也需要考慮在內的,這只是我的一點個人想法,歡迎大家提出自己的看法、

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容