詳解KMP算法

KMP算法應該是每一本《數據結構》書都會講的,算是知名度最高的算法之一了,但很惋惜,我大二那年壓根就沒看懂過~~~算法

以後也在不少地方也都常常看到講解KMP算法的文章,看久了好像也知道是怎麼一回事,但總感受有些地方本身仍是沒有徹底懂明白。這兩天花了點時間總結一下,有點小體會,我但願能夠經過我本身的語言來把這個算法的一些細節梳理清楚,也算是考驗一下本身有真正理解這個算法。數組

 

什麼是KMP算法:數據結構

KMP是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同時發現的。其中第一位就是《計算機程序設計藝術》的做者!!設計

KMP算法要解決的問題就是在字符串(也叫主串)中的模式(pattern)定位問題。說簡單點就是咱們平時常說的關鍵字搜索。模式串就是關鍵字(接下來稱它爲P),若是它在一個主串(接下來稱爲T)中出現,就返回它的具體位置,不然返回-1(經常使用手段)。指針

 

首先,對於這個問題有一個很單純的想法:從左到右一個個匹配,若是這個過程當中有某個字符不匹配,就跳回去,將模式串向右移動一位。這有什麼難的?code

咱們能夠這樣初始化:blog

 

以後咱們只須要比較i指針指向的字符和j指針指向的字符是否一致。若是一致就都向後移動,若是不一致,以下圖:字符串

 

 

A和E不相等,那就把i指針移回第1位(假設下標從0開始),j移動到模式串的第0位,而後又從新開始這個步驟:get

 

基於這個想法咱們能夠獲得如下的程序:數學

/**

 * 暴力破解法

 * @param ts 主串

 * @param ps 模式串

 * @return 若是找到,返回在主串中第一個字符出現的下標,不然爲-1

 */

public static int bf(String ts, String ps) {

    char[] t = ts.toCharArray();

    char[] p = ps.toCharArray();

    int i = 0; // 主串的位置

    int j = 0; // 模式串的位置

    while (i < t.length && j < p.length) {

       if (t[i] == p[j]) { // 當兩個字符相同,就比較下一個

           i++;

           j++;

       } else {

           i = i - j + 1; // 一旦不匹配,i後退

           j = 0; // j歸0

       }

    }

    if (j == p.length) {

       return i - j;

    } else {

       return -1;

    }

}

上面的程序是沒有問題的,但不夠好!(想起我高中時候數字老師的一句話:我不能說你錯,只能說你不對~~~)

若是是人爲來尋找的話,確定不會再把i移動回第1位,由於主串匹配失敗的位置前面除了第一個A以外再也沒有A,咱們爲何能知道主串前面只有一個A?由於咱們已經知道前面三個字符都是匹配的!(這很重要)。移動過去確定也是不匹配的!有一個想法,i能夠不動,咱們只須要移動j便可,以下圖:

 

上面的這種狀況仍是比較理想的狀況,咱們最多也就多比較了再次。但假如是在主串「SSSSSSSSSSSSSA」中查找「SSSSB」,比較到最後一個才知道不匹配,而後i回溯,這個的效率是顯然是最低的。

 

大牛們是沒法忍受「暴力破解」這種低效的手段的,因而他們三個研究出了KMP算法。其思想就如同咱們上邊所看到的同樣:「利用已經部分匹配這個有效信息,保持i指針不回溯,經過修改j指針,讓模式串儘可能地移動到有效的位置。」

 

因此,整個KMP的重點就在於當某一個字符與主串不匹配時,咱們應該知道j指針要移動到哪

 

接下來咱們本身來發現j的移動規律:

 

如圖:C和D不匹配了,咱們要把j移動到哪?顯然是第1位。爲何?由於前面有一個A相同啊:

 

以下圖也是同樣的狀況:

 

能夠把j指針移動到第2位,由於前面有兩個字母是同樣的:

 

至此咱們能夠大概看出一點端倪,當匹配失敗時,j要移動的下一個位置k。存在着這樣的性質:最前面的k個字符和j以前的最後k個字符是同樣的

若是用數學公式來表示是這樣的

P[0 ~ k-1] == P[j-k ~ j-1]

這個至關重要,若是以爲很差記的話,能夠經過下圖來理解:

 

弄明白了這個就應該可能明白爲何能夠直接將j移動到k位置了。

由於:

當T[i] != P[j]時

有T[i-j ~ i-1] == P[0 ~ j-1]

由P[0 ~ k-1] == P[j-k ~ j-1]

必然:T[i-k ~ i-1] == P[0 ~ k-1]

公式很無聊,能看明白就好了,不須要記住。

這一段只是爲了證實咱們爲何能夠直接將j移動到k而無須再比較前面的k個字符。

 

好,接下來就是重點了,怎麼求這個(這些)k呢?由於在P的每個位置均可能發生不匹配,也就是說咱們要計算每個位置j對應的k,因此用一個數組next來保存,next[j] = k,表示當T[i] != P[j]時,j指針的下一個位置。

 

不少教材或博文在這個地方都是講得比較含糊或是根本就一筆帶過,甚至就是貼一段代碼上來,爲何是這樣求?怎麼能夠這樣求?根本就沒有說清楚。而這裏偏偏是整個算法最關鍵的地方。

public static 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]) {

           next[++j] = ++k;

       } else {

           k = next[k];

       }

    }

    return next;

}

這個版本的求next數組的算法應該是流傳最普遍的,代碼是很簡潔。但是真的很讓人摸不到頭腦,它這樣計算的依據究竟是什麼?

好,先把這個放一邊,咱們本身來推導思路,如今要始終記住一點,next[j]的值(也就是k)表示,當P[j] != T[i]時,j指針的下一步移動位置

先來看第一個:當j爲0時,若是這時候不匹配,怎麼辦?

 

像上圖這種狀況,j已經在最左邊了,不可能再移動了,這時候要應該是i指針後移。因此在代碼中才會有next[0] = -1;這個初始化。

若是是當j爲1的時候呢?

 

顯然,j指針必定是後移到0位置的。由於它前面也就只有這一個位置了~~~

 

下面這個是最重要的,請看以下圖:

  

 

請仔細對比這兩個圖。

咱們發現一個規律:

當P[k] == P[j]時,

有next[j+1] == next[j] + 1

其實這個是能夠證實的:

由於在P[j]以前已經有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)

這時候現有P[k] == P[j],咱們是否是能夠獲得P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。

即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。

這裏的公式不是很好懂,仍是看圖會容易理解些。

 

那若是P[k] != P[j]呢?好比下圖所示:

 

像這種狀況,若是你從代碼上看應該是這一句:k = next[k];爲何是這樣子?你看下面應該就明白了。

 

如今你應該知道爲何要k = next[k]了吧!像上邊的例子,咱們已經不可能找到[ A,B,A,B ]這個最長的後綴串了,但咱們仍是可能找到[ A,B ]、[ B ]這樣的前綴串的。因此這個過程像不像在定位[ A,B,A,C ]這個串,當C和主串不同了(也就是k位置不同了),那固然是把指針移動到next[k]啦。

 

有了next數組以後就一切好辦了,咱們能夠動手寫KMP算法了:

public static int KMP(String ts, String ps) {

    char[] t = ts.toCharArray();

    char[] p = ps.toCharArray();

    int i = 0; // 主串的位置

    int j = 0; // 模式串的位置

    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 {

           // i不須要回溯了

           // i = i - j + 1;

           j = next[j]; // j回到指定位置

       }

    }

    if (j == p.length) {

       return i - j;

    } else {

       return -1;

    }

}

和暴力破解相比,就改動了4個地方。其中最主要的一點就是,i不須要回溯了。

 

最後,來看一下上邊的算法存在的缺陷。來看第一個例子:

 

顯然,當咱們上邊的算法獲得的next數組應該是[ -1,0,0,1 ]

因此下一步咱們應該是把j移動到第1個元素咯:

 

不難發現,這一步是徹底沒有意義的。由於後面的B已經不匹配了,那前面的B也必定是不匹配的,一樣的狀況其實還發生在第2個元素A上。

顯然,發生問題的緣由在於P[j] == P[next[j]]

因此咱們也只須要添加一個判斷條件便可:

public static 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;

}

好了,至此。KMP算法也結束了。

很奇怪,好像不是很難的東西怎麼就把我困住這麼久呢?

仔細想一想仍是由於本身太浮躁了,之前老是草草應付,不少細節都沒弄清楚,就覺得本身懂了。結果就只能是似懂非懂的。要學東西真的須要靜下心來。

相關文章
相關標籤/搜索