1. 程式人生 > 實用技巧 >【知識點】KMP演算法詳解

【知識點】KMP演算法詳解

KMP演算法

演算法簡介

KMP演算法,即看毛片 \({Knuth-Morris-Pratt}\) 演算法。是由三位電腦科學家 \(D.E.Knuth、J.H.Morris、V.R.Pratt\) 提出的。該演算法可以在 \(O(n+m)\) 的時間複雜度內查詢一個字串在另一個字串中的位置。

KMP演算法的基本原理就是尋找模式串的公共前後綴,以優化時間複雜度。

演算法原理

盜用百度的圖片推薦閱讀

首先,假設我們有一個字串和一個需要比對的“模式串”,如圖:

首先,我們一個華(bao)麗(li)的開頭,就是把模式串與子串進行逐位匹配:

但是我們發現第六個字元不匹配。
按照傳統的思路,我們需要將模式串右移一位,然後繼續諸位比較。但是KMP演算法是一個毒瘤

高階演算法,怎麼能容忍如此赤裸裸的暴力呢?

我們發現目前已經匹配的子串中,字首和字尾都是一樣的,都是“GTG”:

所以我們驚訝地發現,我們可以直接把模式串右移到最長可匹配字尾的位置,然後繼續愉快的比較

可是又出現了一個“壞字元”,我們應該如何處理呢?
沒錯,繼續尋找可匹配最長字首和字尾,然後再次移動模式串。

以此類推。這就是KMP演算法的流程。

原理應該理解了,那麼要如何實現呢?

演算法實現

一、\(nxt\) 陣列

\(nxt\) 是一個一維整型陣列,陣列的下標代表了“已匹配字首的下一個位置”,元素的值則是“最長可匹配字首子串的下一個位置”。如圖所示:

其中,由於子串“G”、“GT”、“GTGTGC”沒有可匹配的字首和字尾,所以對應的 \(nxt\)

值為 \(0\)

只要我們求出 \(nxt\) 陣列,我們就可以解決尋找最長可匹配前後綴並移動模式串的問題了。

相信聰明的你已經理解了

二、求出 \(nxt\) 陣列

我們設兩個變數,\(i\)\(j\) ,它們分別表示“已匹配字首的下一個位置”,也就是待填充的陣列下標,和“最長可匹配字首子串的下一個位置”,也就是待填充的陣列元素值。它們的初始值如下:

求出 \(nxt\) 陣列的過程,就是用模式串“自己匹配自己”。

首先,我們讓 \(i++\)

此時,一匹配子串長度為 \(1\) ,不存在可匹配字尾,故 \(nxt[1]=0\)

我們令 \(i\) 繼續 \(+1\)
可以發現,最長可匹配前後綴子串仍不存在,所以 \(nxt[2]=0\)

\(i\) 又一次加一時,我們發現此時的模式串 \(s\) 中存在 \(s[j]=s[i-1]\) ,於是 \(nxt[3]=nxt[2]+1=1\)

現在,我們需要讓 \(i,\ j\) 都加一。

我們驚訝的發現,\(s[j]=s[i-1]='T'\) ,所以可匹配最長前後綴子串的長度加一,即 \(nxt[4]=nxt[3]+1=2\)

\(i,\ j\) 再次同時加一後,我們找到了 \(s[j]=s[i-1]='G'\),所以 \(nxt[5]=nxt[4]+1=3\)

可是此時的 \(s[j]\)\(s[i-1]\) 不匹配了,怎麼辦呢?

按照套路,應該移動模式串了。如何移動?簡單地移動一位嗎?當然不,我們令 \(j=nxt[j]\)

可是天不從人願,我們發現字元仍然不匹配。所以再次使 \(j=nxt[j]\)

此時, \(j\) 已經無法回溯,所以 \(nxt[6]=0\)\(nxt\) 陣列就求出來了。
推導完畢。匹配的過程也類似。

建議將 \(nxt\) 陣列的推導多看幾遍,這樣可以加深理解。因為 \(nxt\) 陣列的推導過程是KMP演算法最反人類核心的地方。

具體程式碼實現

\(\mathtt{Talking\ is\ cheap,\ show\ me\ the\ code.}\)

void KMP(char *s1,char *s2)
{
    /* KMP演算法
     * @params s1為主串,s2為模式串,l1,l2分別是它們的長度
     * @from 程式碼來自Luogu P3375,是一道KMP模板題
     */
    int l1=strlen(s1),l2=strlen(s2);
    int j=0;
    for(int i=2;i<=l2;i++)
    {
        while(j && s2[i]!=s2[j+1])
            j=nxt[j];
        if(s2[i]==s2[j+1])	j++;
        nxt[i]=j;
    }
    j=0;
    for(int i=1;i<=l1;i++)
    {
        while(j && s1[i]!=s2[j+1])
            j=nxt[j];
        if(s1[i]==s2[j+1])	j++;
        if(j==l2)
            printf("%d\n",i-l2+1),j=nxt[j];
    }
}

注:\(Galax OJ\)\(KMP\) 練習題題解已經在路上啦~

恭喜你找到了練習題連結~