KMP算法詳解

尊重原作者,原文地址:http://blog.csdn.net/shakespeare001/article/details/51381251

0、關於KMP

KMP是用於字符匹配的一個常用算法。關於KMP概念、前綴、後綴概念參考文章中有詳細介紹,這裏就不做詳細討論,本文詳細關注討論KMP核心點,next數組的作用及求解思路,KMP算法的思路。

1、next數組是什麼

next數組裏面存放的是要查找的字符串前i個字符串的所有前綴、後綴相等的公共串中,最大的長度值。比如需要查找的一個子串ababcd,next[0]表示子串中前1個字符串即a的前綴和後綴中相等字符串的最大長度,因爲a的前綴和後綴沒有,故next[0] = 0;對於next[2],即先求出子串aba的前綴和後綴出來,前綴爲a,ab,後綴有ba,a,相等的公共串爲a,長度爲1,因此next[2] = 1;依次可以求出。

2、next數組的作用是什麼

    在我們用暴力解決字符串匹配的過程中,當我們主串的下一個字符和子串的下一個字符不相等時,此時,主串指針需要回退到最近的一個字符和子串第一個字符相等的下一個位置繼續匹配。當後面有一個字符和子串第一個字符相等時,再來一遍上述的匹配過程。整個過程中,主串指針會回退很多遍,導致我們暴力破解的複雜度達到了O(n*m)。
    我們再來考慮一下上面的情形,當我們主串的下一個字符和子串的下一個字符不相等時,我們主串前面的一些字符是和子串經過了匹配的,而且匹配成功了,如下圖:


如果按照暴力方式,主串此時需要回退到第4個位置處的b處,繼續進行掃描判斷。爲了優化這個過程,我們現在來考察子串,對於子串前q-1個字符串s,即s = abcab:
(1)如果字符串s存在着相等的前綴和後綴(即next[q-1]>0),這個時候我們不需要去回退主串指針,只要把我們的子串往後移動,使得我們字符串s中的前綴和後綴相等的那個前綴移動到後綴的位置處,因爲前綴=後綴=主串中的部分串,這樣我們直接就將子串定位到了可以進行下一趟比較探測的位置,而沒有回退過主串指針。我們希望這個相等的前綴和後綴的長度越大越好(這也是爲什麼我們next數組中是求相等的前綴和後綴中最長的那個相等串的值),顯然這樣就可以匹配更多相同的元素。對於上面的例子來說就是,s = abcab,此時前綴和後綴相等的只有ab,也就是我們的next[q-1] = 2;這個時候,我們移動子串,使得我們的前綴ab移動到後綴ab處的位置,如下:


這樣,我們就可以在不會退主串指針的情況下,繼續進行下一趟探測比較了。上面子串的移動距離也很好計算,就是前面已經匹配的子串的字符數量-next[q-1],即5-2=3。

(2)如果字符串s中不存在相等的前綴和後綴(即next[q-1]=0),這個時候就更好辦了,按照上面這個移動距離公式,可以計算出移動的距離是5-0 = 5,相當於直接移動到了i指針的下面。這裏爲什麼可以這麼移動,是因爲這種情況下即使我們回退指針,在再次到i之前,我們也不能找到能夠匹配到的字符串。

可以看到,我們next數組的作用,就是在我們匹配失敗的時候,確定我們子串需要往後移動的距離,而避免我們的主串指針進行回退。這樣可以保證主串在只遍歷一遍的情況下找到子串。

因此KMP算法的重點就是如何快速的求出這個next數組出來。

3、求出next數組

經過上面的分析,知道next數組是隻與子串有關與主串無關的,它記錄的是子串到每個字符處那個公共前綴(或後綴)的最大長度。因此,我們現在主要就是要對子串來求出其next數組。
假設我們已經準備計算第i個位置字符的next值。我們可以利用前面已經計算出來的next值進行求解。假設已經求出的next[i-1] = k,即子串從開始到i-1處這段字符串中,最大的相等的前綴和後綴長度爲k,如下圖中的第二排所示:


這個時候,我們需要計算到第i個字符處next值,這個時候就有兩種情況了(假設待查子串的字符串以str變量表示):
(1)如果str[i] == str[k],這種情況下,前綴往後再加上一個字符之後依然會和後綴往後加上一個字符相等,因爲此時前綴和後綴加上的是同一個字符。因此,此時next[i] = next[i-1] + 1,即next[i] = k+1
對於上圖來說,就是前綴a...b加上一個字符d後,變爲a..bd,後綴加上一個字符c後,變爲a..bc(這裏是不等的,只是爲了說明一下)。
(2)如果str[i] != str[k],說明前、後綴分別加上一個字符擴展之後是不相同了,這個時候,a...b這一段是不能再用了,也就是next[i-1]的值沒有考察意義了,也即k此時需要調整。那就只能縮小範圍,前綴要往前收縮,後綴要往後收縮。因爲此時,前綴和後綴是相同的字符串(即如上圖中的前面k個字符串a...b,和後面k個字符串a...b是相同的,因此只要在前綴字符串中找出新的前綴和後綴,這個新的前綴=新的後綴=原來的後綴的一小部分)。如下圖所示:


注意:上面的k有兩層含義,一個指的是next[index]中的值,表示到第index處字符串相等前綴和後綴的最大長度;另一個是,由前一層含義可知最大相等的前綴長度爲k,也就可以用這個k作爲下標索引值,即前綴是從str[0]...str[k-1]處

如上圖,繼續分析,如果str[i] != str[k],此時需要將k個字符串前綴a...b進行劃分,先考察k-1處的next值,令k' = next[k-1],即a...b劃分成瞭如下所示的樣子:


這時,繼續判斷str[i] == str[k'],輪迴到上面兩種情況的判斷,如果相等,即可以直接確定next[i]的值,不等又繼續對str[0]...str[k'-1]處進行劃分,依次下去,直到i遍歷完子串。

因此,根據上述思路,我們可以編寫出以下代碼來實現:   
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. private int[] getNextArray(char[] chs){  
  2.        int i;//字符數組的下標指示器  
  3.        int k;//前一個字符處的最大公共(相等)前、後綴子串的長度  
  4.        int[] next = new int[chs.length];  
  5.        for(i = 1,k = 0; i < chs.length; i++){  
  6.            while(k > 0 && chs[i] != chs[k])    //此處的k可以作爲上面講到的第一層含義理解  
  7.                k = next[k - 1];      
  8.            if(chs[i] == chs[k]){  
  9.                k++;  
  10.            }  
  11.            next[i] = k;  
  12.        }  
  13.        return next;  
  14.    }  

4、KMP實現

上面我們把一個重要問題next數組的求解問題解決了。下面就可以開始KMP的實現了,KMP的步驟或原理其實在第2點中 “next數組的作用是什麼”已經體現出來了。我們對比第2點和第3點中next數組的求解,發現其實進行next數組求解的過程,類似於主串和子串進行匹配的過程,只不過是在next數組求解過程中,是子串和子串自己進行比較而已。因此整個KMP算法的代碼過程如下:    
[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public boolean kmp(String str1,String str2){  
  2.         char[] strA = str1.toCharArray();  
  3.         char[] strB = str2.toCharArray();  
  4.         int[] next = getNextArray(strB);    //獲取需要匹配子串的next數組  
  5.         int i,k;  
  6.         for(i = 0,k = 0; i < strA.length; i++){  
  7.             while(k > 0 && strA[i] != strB[k])  
  8.                 k = next[k-1];  
  9.             if(strA[i] == strB[k]){  
  10.                 k++;  
  11.             }  
  12.             /* 
  13.              * 注意,這裏和求next數組有一點區別,因爲kmp裏面是主串和子串進行比較,當子串最後一個元素都相等的時候,k就相當於是子串和主串相同的公共部分長度, 
  14.              * 而對於求next數組中的方法來說,相當於是自身和自身進行比較 
  15.              */  
  16.             if(k == strB.length){  
  17.                 return true;  
  18.             }  
  19.         }  
  20.         return false;  
  21.     }  
  22.   
  23.     //獲取next數組  
  24.     private int[] getNextArray(char[] chs){  
  25.         int i;//字符數組的下標指示器  
  26.         int k;//前一個字符處的最大公共(相等)前、後綴子串的長度  
  27.         int[] next = new int[chs.length];  
  28.         for(i = 1,k = 0; i < chs.length; i++){  
  29.             while(k > 0 && chs[i] != chs[k])    //此處的k可以作爲上面講到的第一層含義理解  
  30.                 k = next[k - 1];      
  31.             if(chs[i] == chs[k]){  
  32.                 k++;  
  33.             }  
  34.             next[i] = k;  
  35.         }  
  36.         return next;  
  37.     }  
經過測試,符合要求。

我們比較上面兩個方法的代碼,正如我們前面所說的那樣,KMP過程和next數組求解過程非常類似,因此這兩個方法非常類似,可以說,next數組求解過程就是自身匹配(或自身KMP)的過程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章