一篇文章教你徹底理解用于字符串匹配的KMP算法

KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt三人同時發現,因此人們稱它為Knuth-Morris-Pratt算法(簡稱KMP)。KMP算法的關鍵是利用匹配失敗后的信息,盡量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現依賴一個next()函數,函數本身包含了模式串的局部匹配信息,其時間復雜度為O(m+n)。
網上有很多文章講解KMP算法,但都不夠清晰透徹、通俗易懂,尤其在介紹最令人困惑的next()函數時,解釋拗口,模棱兩可,讓讀者理解起來頗為費勁。本人通過閱讀各路大神的學習筆記,汲取其中精華部分,并結合自我理解,帶你全面剖析KMP算法思想及實現,希望對學習KMP算法仍有盲區和疑惑的同學有所幫助。

KMP算法的原理

舉例來說,有一個字符串”BBC ABCDAB ABCDABCDABDE”(稱為主串),我想知道,里面是否包含另一個字符串”ABCDABD”(稱為模式串)?
1.首先,主串”BBC ABCDAB ABCDABCDABDE”的第一個字符與模式串”ABCDABD”的第一個字符,進行比較。因為B與A不匹配,所以模式串需要后移一位。

2.因為B與A不匹配,模式串需要再往后移。


3.就這樣,直到主串有一個字符,與模式串的第一個字符相同為止。


4.接著比較主串和模式串的下一個字符,發現還是相同。


5.直到主串有一個字符,與模式串對應的字符不相同為止,如下圖所示。


6.這時,最自然的反應是,將模式串整個后移一位,再從頭逐個比較。這樣做雖然可行,但是效率很差,因為你要把”搜索位置”移到已經比較過的位置,重比一遍。


7.一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是”ABCDAB”。KMP算法的想法是,設法利用這個已知信息,不要把”搜索位置”移回已經比較過的位置,繼續把它向后移,這樣就提高了效率。


8.怎么做到這一點呢?可以針對模式串,算出一張《部分匹配表》(Partial Match Table)。這張表是如何產生的,后面再介紹,這里只要會用就可以了。


9.如下圖所示,已知空格與D不匹配時,前面六個字符”ABCDAB”是匹配的。查表可知,”ABCDAB”中最后一個匹配字符B對應的”部分匹配值”為2,因此按照下面的公式算出向后移動的位數:
 移動位數 = 已匹配的字符數 - 對應的部分匹配值

因為 6 - 2 等于4,所以將模式串整體向后移動4位。
10.如下圖,因為空格與C不匹配,模式串還要繼續往后移。這時,已匹配的字符數為2(”AB”),對應的”部分匹配值”為0。所以,移動位數 = 2 - 0,結果為 2,于是將模式串向后移2位。


11.如下圖所示,因為空格與A不匹配,需要繼續后移一位。


12.逐位比較,直到發現C與D不匹配。于是,移動位數 = 6 - 2,繼續將模式串向后移動4位。


13.逐位比較,直到模式串的最后一位,發現完全匹配,于是搜索完成。如果還要繼續搜索(即找出全部匹配),移動位數 = 7 - 0,再將模式串向后移動7位,這里就不再重復了。

部分匹配表

下面介紹《部分匹配表》是如何產生的。


首先,要了解兩個概念:”前綴”和”后綴”。 “前綴”指除了最后一個字符以外,一個字符串的全部頭部組合;”后綴”指除了第一個字符以外,一個字符串的全部尾部組合。

“部分匹配值”就是”前綴”和”后綴”的最長的共有元素的長度。以”ABCDABD”為例,

- "A"的前綴和后綴都為空集,共有元素的長度為0;
- "AB"的前綴為[A],后綴為[B],共有元素的長度為0;
-  "ABC"的前綴為[A, AB],后綴為[BC, C],共有元素的長度0;
-  "ABCD"的前綴為[A, AB, ABC],后綴為[BCD, CD, D],共有元素的長度為0;
-  "ABCDA"的前綴為[A, AB, ABC, ABCD],后綴為[BCDA, CDA, DA, A],共有元素為"A",長度為1;
-  "ABCDAB"的前綴為[A, AB, ABC, ABCD, ABCDA],后綴為[BCDAB, CDAB, DAB, AB, B],共有元素為"AB",長度為2;
-  "ABCDABD"的前綴為[A, AB, ABC, ABCD, ABCDA, ABCDAB],后綴為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度為0。

按照定義,”ABCDAB”的部分匹配表即如下所示:


“部分匹配值”的實質是,有時候,字符串頭部和尾部會有重復。比如,”ABCDAB”之中有兩個”AB”,那么它的”部分匹配值”就是2(”AB”的長度)。模式串移動的時候,第一個”AB”向后移動4位(字符串長度-部分匹配值),就可以來到第二個”AB”的位置。

理解next數組

理解KMP算法的核心和難點就是理解next數組的巧妙設計,下面我會重點解釋next數組的含義。
next數組,又叫做“失配函數”,它是以下標 0 開始的數組,為了方便大家理解,給出如下圖示:


根據 KMP 算法,在失配位會調用該位的 next 數組的值,下面我將詳細道出next數組的來龍去脈。

next[i]表示在失配位i之前的最長公共前后綴的長度。

首先,我們取之前已經匹配的部分(即藍色的那部分!)


我們在上面說到“最長公共前后綴”,體現到下圖所示的樣子。


next數組的作用是通過尋找最長公共前后綴的部分,快速移動模式串,從而提高字符串匹配的效率,如下圖所示:


next[i]返回當前位置i的最長公共前后綴的長度,假設為 len 。因為數組是由 0 開始的,所以 next 數組讓模式串的第 len 位與主串匹配就是拿最長前綴之后的第 1 位與失配位重新匹配,避免匹配串從頭開始,如下圖所示。


如果上圖中的紅色位置依然匹配失效,則需要對上圖中的綠色部分再次去求解它的最長公共前后綴長度(假設為len’),然后繼續向右移動模式串,讓模式串的第 len’ 位與主串的失配位重新進行匹配,如果仍舊不匹配,則繼續以上過程操作。如下圖所示:


我們發現,當發生失配的時候,可以借助遞推的思想,根據已知的結果繼續求出當前失配位之前的最長公共前后綴的長度,然后,繼續移動模式串,從而進行新一輪的字符串匹配。

解釋這么多,那么next數組究竟如何求出呢?

我們需要分兩種情況考慮。


1.當紅色部分相同(即S[k]==S[q])時,則當前 next 數組的值為上一次 next 的值加一(即next[q] = k++),如上圖所示。

2.當紅色部分不等的時候,則需要對綠色部分遞推求解 k’ = next[k-1],然后再對新的 k’ 位置字符與 q 位置字符進行匹配,如果相等,則 next[q] = k’+1,否則,執行遞推匹配,直到k’=0時遞推結束。比如,模式串“ABCABXABCABC”,最后一個字符C的next數組值為3。(因為C之前的最長公共前后綴為“ABCAB”,而“ABCAB”的最長公共前后綴為“AB”,其長度為2,又源于第三個字符C與最后一個字符C匹配,所以最后一個字符C的next數組值為3)

代碼實現

創建文件kmp.c,內容如下:

#include<stdio.h>
#include<string.h>

void makeNext(const char P[],int next[])
{    
    int q,k;    
    int m = strlen(P);
    next[0] = 0;    
    for (q = 1,k = 0; q < m; ++q)
    {        
        while(k > 0 && P[q] != P[k])
            k = next[k-1];        
        if (P[q] == P[k])
        {            
            // 上一次的next值+1
            k++;
        }
        next[q] = k;
    }
}
void kmp(const char T[],const char P[],int next[])
{    
    int n,m;    
    int i,q;
    n = strlen(T);
    m = strlen(P);
    makeNext(P,next);    
    for (i = 0,q = 0; i < n; ++i)
    {        
        while(q > 0 && P[q] != T[i])
            q = next[q-1];        
        if (P[q] == T[i])
        {
            q++;
        }        
        if (q == m)
        {
            q=0;            
            printf("Pattern occurs with shift: %d\n",(i-m+1));
        }
    }
}
int main()
{    
    int i;    
    int next[20]={0};    
    char T[] = "BBC ABCDABD ABCDABCDABDE";    
    char P[] = "ABCDABD";    
    printf("主串:%s\n",T);    
    printf("模式串:%s\n",P );    
    kmp(T,P,next);   
    printf("next數組:");    
    for (i = 0; i < strlen(P); ++i)
    {        
        printf("%d ",next[i]);
    }    
    printf("\n");    
    return 0;
}

保存后,在終端執行如下編譯命令:

$ gcc -o kmp kmp.c
$ ./kmp
# 其運行結果如下:

主串:BBC ABCDABD ABCDABCDABDE
模式串:ABCDABD
Pattern occurs with shift: 4
Pattern occurs with shift: 16
next數組:0 0 0 0 1 2 0

總結

理解KMP算法的難點就在于理解next數組的實現,在遇到失配位時能夠靈活地應用遞推方法,根據已知的結果,進一步求解出子最長公共前后綴的長度,然后進一步的完成新一輪的匹配,從而避免從頭開始,極大提高了匹配速率。kmp算法的時間復雜度O(n+m),可以采用均攤分析來解答,具體可參考算法導論。

參考文章:
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html 作者-阮一峰
http://www.tuicool.com/articles/e2Qbyyf

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容