KMP演算法詳解及其Java實現

來源:互聯網
上載者:User

標籤:fonts   思路   例子   複習   分享圖片   首碼和   mat   void   大量   

KMP演算法,又稱作“看貓片”演算法(誤),是一種改進的字串模式比對演算法,可以在O(n+m)的時間複雜度以內完成字串的匹配操作,其核心思想在於:當一趟匹配過程中出現字元不匹配時,不需要回溯主串的指標,而是利用已經得到的“部分匹配”,將模式串儘可能多地向右“滑動”一段距離,然後繼續比較。

KMP(看貓片)演算法

1. 樸素的字串模式比對演算法

求一個字串(模式串)在另一個字串(主串)中的位置,稱為字串模式比對。

在樸素的字串模式比對演算法中,我們對主串S和模式串T分別設定指標i和j,假設字串下標從0開始,初始時i和j分別指向每個串的第0個位置。在第n趟匹配開始時,i指向主串S中的第n-1個位置,j指向模式串T的第0個位置,然後逐個向後比較。若T中的每一個字元都與S中的字元相等,則稱匹配成功,否則,當遇到某個字元不相等時,i重新指向S的第n個位置,j重新指向T的第0個位置,繼續進行第n+1趟匹配。

例如,我們對模式串T=“abaabcac”和主串S=“abcabaabaabcacb”進行匹配。1.1,此時進行中第4趟匹配,S[3...7]與T[0...4]均相等,但當i=8,j=5時,S[8]與T[5]不相等,匹配失敗。於是,置i=4,j=0,相當於將模式串向右移動一位後,重新開始下一趟匹配,1.2。


圖1.1 當i=8,j=5時,字元不相等,匹配失敗

圖1.2 將模式串向右移動一位後,重新開始下一趟匹配
利用此種方法進行字串匹配,最壞情況下時間複雜度為O(n*m),其中n和m分別為主串和模式串的長度。

2. 改進的字串模式比對演算法——KMP演算法

在上面的例子中,我們可以看到,當i=8,j=5時,S[8]與T[5]不相等,於是置i=4,j=0,相當於將模式串向右移動一位,再開始下一趟匹配。然而,通過觀察我們可以發現,之後的兩趟匹配,即i=4,j=0以及i=5,j=0都是不必要的。這是因為,在之前的一趟匹配過程中,我們已經部分匹配了T的子串“abaab”。此時將T向右移動一位,則相當於對T中的“abaab……”與S中的“baab……”進行匹配,顯然無法匹配成功。繼續右移T,則相當於對T中的“abaab……”與S中的“aab……”進行匹配,依然無法匹配成功。只有當T向右移動3位後,此時對T中的“abaab……”與S中的“ab……”進行匹配,才會有成功的可能,也就有必要向後繼續進行比較。2.1。


圖2.1 匹配失敗時,將T向右移動3位後,才有繼續比較的必要
因此,當i=8,j=5,T的子串“abaab”已經匹配成功,而其後一位字元卻不相等時,不必回溯i指標,置i=8,j=2,繼續向後比較,相當於將T向右移動3位,並從T的第3位開始向後比較。2.2。

圖2.2 匹配失敗後,直接置i=8,j=2,繼續向後比較

這就是KMP演算法的基本思路。對於模式串T中的前j個字元組成的子串,設定數組next[j]存放一個值,當模式串T匹配至第j個字元時與主串不相等,則i指標不變,將j指標置為next[j]的值,然後繼續進行比較。在上例中,串“abaab”為模式串T的前5個字元組成的子串,令next[5]=2,當i=8,j=5時,S[8]與T[5]不相等,於是置i=8,j=next[j]=next[5]=2,然後繼續進行比較。

因此,KMP演算法的核心在於求出數組next,即模式串T中每一個長度為j (0<j<T.length) 的首碼所對應的next[j]的值。

next數組求解演算法

在求解next數組前,我們首先需要理解next數組的含義。回到前面的例子,當T的子串“abaab”的下一個字元與主串不相等時,主串的指標i不變,j回溯至2,指向T的第3個字元,其本質是因為串“abaab”的首碼和尾碼有一個長度為2的最長公用串“ab”,因此我們省略了首碼“ab”和尾碼“ab”的比較過程,直接對它們的後一個字元,即T[2]和S[8]進行比較。

再看另一個例子,假設有模式串T=“abacaabadad”,其已部分匹配完T[0...7],即“abacaaba”,在匹配T[8]時遇到匹配失敗,因T[0...7]的首碼和尾碼有長度為3的最長公用串“aba”,因此next[8]=3,置j=next[j]=next[8]=3,i不變,然後從T[3],即T的第4個字元開始比較。2.3。


圖2.3 匹配T[8]時失敗,i不變,j回溯至3

總之,對於模式串T,next[j]代表了T的前j個字元組成的子串中,其首碼和尾碼的最長公用串的長度。

求解字串T的next數組的演算法如下:

  1. next[0]=-1, next[1]=0。
  2. 在求解next[j]時,令k=next[j-1],
  3. 比較T[j-1]與T[k]的值,

    a. 若T[j-1]等於T[k],則next[j]=k+1。

    b. 若T[j-1]不等於T[k],令k=next[k],若k等於-1,則next[j]=0,否則跳至3。

下面以模式串T=“abaabcac”為例,給出求next數組的過程:

  1. next[0]=-1, next[1]=0。
  2. 當j=2時,k=next[j-1]=next[1]=0,由於T[j-1]=T[1]=‘b’,T[k]=T[0]=‘a’,T[j-1]不等於T[k],令k=next[k]=next[0]=-1,因此next[2]=0。
  3. 當j=3時,k=next[j-1]=next[2]=0,由於T[j-1]=T[2]=‘a’,T[k]=T[0]=‘a’,T[j-1]等於T[k],因此next[3]=k+1=1。
  4. 當j=4時,k=next[j-1]=next[3]=1,由於T[j-1]=T[3]=‘a’,T[k]=T[1]=‘b’,T[j-1]不等於T[k],令k=next[k]=next[1]=0。此時T[k]=T[0]=‘a’,T[j-1]等於T[k],因此next[3]=k+1=1。
  5. 當j=5時,k=next[j-1]=next[4]=1,由於T[j-1]=T[4]=‘b’,T[k]=T[1]=‘b’,T[j-1]等於T[k],因此next[5]=k+1=2。
  6. 當j=6時,k=next[j-1]=next[5]=2,由於T[j-1]=T[5]=‘c’,T[k]=T[2]=‘a’,T[j-1]不等於T[k],令k=next[k]=next[2]=0。此時T[k]=T[0]=‘a’,T[j-1]不等於T[k],再令k=next[k]=next[0]=-1,因此next[6]=0。
  7. 當j=7時,k=next[j-1]=next[6]=0,由於T[j-1]=T[6]=‘a’,T[k]=T[0]=‘a’,T[j-1]等於T[k],因此next[7]=k+1=1。

將next數組全部求出之後,只需在簡單的匹配演算法上稍作修改,便得到了KMP的匹配演算法:當模式串T匹配至第j個字元時匹配失敗,i指標不變,將j指標置為next[j]的值,若j的值為-1,則將i和j同時加1。隨後繼續進行逐個的比較。

下面以模式串T=“abaabcac”和主串S=“abcabaabaabcacb”進行匹配為例,給出KMP匹配演算法的全過程。
之前已經求得模式串T的next數組為[-1, 0, 0, 1, 1, 2, 0, 1]。

  1. 初始時,i=0,j=0,匹配成功。
  2. i=1,j=1,匹配成功。
  3. i=2,j=2,匹配失敗。
  4. i=2,j=next[2]=0,匹配失敗。
  5. i=2,j=next[0]=-1,匹配失敗。
  6. i=2+1=3,j=-1+1=0,匹配成功。
  7. i=4,j=1,匹配成功。
  8. i=5,j=2,匹配成功。
  9. i=6,j=3,匹配成功。
  10. i=7,j=4,匹配成功。
  11. i=8,j=5,匹配失敗。
  12. i=8,j=next[5]=2,匹配成功。
  13. 繼續向後比較,中間過程均匹配成功,故不再贅述,當i=13,j=7時,模式串匹配完成。

以上就是KMP匹配演算法的全過程。總結一下,KMP演算法的實質就是以空間換時間,在匹配之前將模式串的一些資訊儲存起來(next數組),在隨後的匹配過程中利用這些資訊減少不必要的匹配次數,以提高匹配效率。在實際的應用過程中,簡單模式比對演算法的執行時間常常接近於KMP演算法,僅當主串與模式串有很多“部分匹配”時,KMP演算法才能顯著提升效能。

3. KMP演算法的Java實現

下面給出KMP演算法的Java代碼。整個演算法分為兩部分,一是next數組的求解,二是KMP匹配過程。

public class KMP {    /**     * 求出一個字元數組的next數組     * @param t 字元數組     * @return next數組     */    public static int[] getNextArray(char[] t) {        int[] next = new int[t.length];        next[0] = -1;        next[1] = 0;        int k;        for (int j = 2; j < t.length; j++) {            k=next[j-1];            while (k!=-1) {                if (t[j - 1] == t[k]) {                    next[j] = k + 1;                    break;                }                else {                    k = next[k];                }                next[j] = 0;  //當k==-1而跳出迴圈時,next[j] = 0,否則next[j]會在break之前被賦值            }        }        return next;    }    /**     * 對主串s和模式串t進行KMP模式比對     * @param s 主串     * @param t 模式串     * @return 若匹配成功,返回t在s中的位置(第一個相同字元對應的位置),若匹配失敗,返回-1     */    public static int kmpMatch(String s, String t){        char[] s_arr = s.toCharArray();        char[] t_arr = t.toCharArray();        int[] next = getNextArray(t_arr);        int i = 0, j = 0;        while (i<s_arr.length && j<t_arr.length){            if(j == -1 || s_arr[i]==t_arr[j]){                i++;                j++;            }            else                j = next[j];        }        if(j == t_arr.length)            return i-j;        else            return -1;    }    public static void main(String[] args) {        System.out.println(kmpMatch("abcabaabaabcacb", "abaabcac"));    }}
參考資料及致謝

在學習KMP演算法思路的過程中,我大量參考了“王道考研系列”的《資料結構聯考複習指導》一書,以及CSDN博主v_JULY_v的文章:從頭到尾徹底理解KMP,特此鳴謝。

同時感謝微博博主@回憶專用小馬甲和實驗室的大紅袍CocoXu提供的大量貓片,讓我在學習KMP演算法的過程中擁有持續的動力。

文章中如有錯誤,歡迎指正!

KMP演算法詳解及其Java實現

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.