KMP算法詳解及其Java實現


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

![KMP(看貓片)算法](https://images2018.cnblogs.com/blog/1485189/201809/1485189-20180909160802638-149491311.jpg)
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](https://images2018.cnblogs.com/blog/1485189/201809/1485189-20180909160938585-1146094368.png)
圖1.1 當i=8,j=5時,字符不相等,匹配失敗
![圖1.2](https://images2018.cnblogs.com/blog/1485189/201809/1485189-20180909161818355-942371414.png)
圖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](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107202953821-445138636.png)
圖2.1 匹配失敗時,將T向右移動3位后,才有繼續比較的必要
因此,當i=8,j=5,T的子串“abaab”已經匹配成功,而其后一位字符卻不相等時,不必回溯i指針,置i=8,j=2,繼續向后比較,相當於將T向右移動3位,並從T的第3位開始向后比較。如圖2.2。
![圖2.2](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203048317-1520055718.png)
圖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](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203117367-612235735.png)
圖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[4]=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,匹配成功。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203144421-1790051929.png)
2. i=1,j=1,匹配成功。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203301705-1573255069.png)
3. i=2,j=2,匹配失敗。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203312482-506404756.png)
4. i=2,j=next[2]=0,匹配失敗。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203321486-1584379594.png)
5. i=2,j=next[0]=-1,匹配失敗。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203329813-1181552146.png)
6. i=2+1=3,j=-1+1=0,匹配成功。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203345507-1570749968.png)
7. i=4,j=1,匹配成功。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203356011-1340913809.png)
8. i=5,j=2,匹配成功。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203406410-1127902417.png)
9. i=6,j=3,匹配成功。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203417066-1414119806.png)
10. i=7,j=4,匹配成功。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203426091-1146892290.png)
11. i=8,j=5,匹配失敗。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203437862-1315035527.png)
12. i=8,j=next[5]=2,匹配成功。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203448412-1574307342.png)
13. 繼續向后比較,中間過程均匹配成功,故不再贅述,當i=13,j=7時,模式串匹配完成。
![](https://img2018.cnblogs.com/blog/1485189/201811/1485189-20181107203458300-1121595444.png)

以上就是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算法的過程中擁有持續的動力。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM