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

KMP算法是一種改進(jìn)的字符串匹配算法亮靴,由D.E.Knuth,J.H.Morris和V.R.Pratt三人同時(shí)發(fā)現(xiàn)况既,因此人們稱(chēng)它為Knuth-Morris-Pratt算法(簡(jiǎn)稱(chēng)KMP)昼弟。KMP算法的關(guān)鍵是利用匹配失敗后的信息彭羹,盡量減少模式串與主串的匹配次數(shù)以達(dá)到快速匹配的目的。具體實(shí)現(xiàn)依賴(lài)一個(gè)next()函數(shù)岛啸,函數(shù)本身包含了模式串的局部匹配信息钓觉,其時(shí)間復(fù)雜度為O(m+n)。
網(wǎng)上有很多文章講解KMP算法坚踩,但都不夠清晰透徹荡灾、通俗易懂,尤其在介紹最令人困惑的next()函數(shù)時(shí)瞬铸,解釋拗口批幌,模棱兩可,讓讀者理解起來(lái)頗為費(fèi)勁嗓节。本人通過(guò)閱讀各路大神的學(xué)習(xí)筆記荧缘,汲取其中精華部分,并結(jié)合自我理解拦宣,帶你全面剖析KMP算法思想及實(shí)現(xiàn)截粗,希望對(duì)學(xué)習(xí)KMP算法仍有盲區(qū)和疑惑的同學(xué)有所幫助信姓。

KMP算法的原理

舉例來(lái)說(shuō),有一個(gè)字符串”BBC ABCDAB ABCDABCDABDE”(稱(chēng)為主串)绸罗,我想知道意推,里面是否包含另一個(gè)字符串”ABCDABD”(稱(chēng)為模式串)?
1.首先从诲,主串”BBC ABCDAB ABCDABCDABDE”的第一個(gè)字符與模式串”ABCDABD”的第一個(gè)字符左痢,進(jìn)行比較。因?yàn)锽與A不匹配系洛,所以模式串需要后移一位俊性。

2.因?yàn)锽與A不匹配,模式串需要再往后移描扯。


3.就這樣定页,直到主串有一個(gè)字符,與模式串的第一個(gè)字符相同為止绽诚。


4.接著比較主串和模式串的下一個(gè)字符典徊,發(fā)現(xiàn)還是相同。


5.直到主串有一個(gè)字符恩够,與模式串對(duì)應(yīng)的字符不相同為止卒落,如下圖所示。


6.這時(shí)蜂桶,最自然的反應(yīng)是儡毕,將模式串整個(gè)后移一位,再?gòu)念^逐個(gè)比較扑媚。這樣做雖然可行腰湾,但是效率很差,因?yàn)槟阋选彼阉魑恢谩币频揭呀?jīng)比較過(guò)的位置疆股,重比一遍费坊。


7.一個(gè)基本事實(shí)是,當(dāng)空格與D不匹配時(shí)旬痹,你其實(shí)知道前面六個(gè)字符是”ABCDAB”附井。KMP算法的想法是,設(shè)法利用這個(gè)已知信息两残,不要把”搜索位置”移回已經(jīng)比較過(guò)的位置羡忘,繼續(xù)把它向后移,這樣就提高了效率磕昼。


8.怎么做到這一點(diǎn)呢卷雕?可以針對(duì)模式串,算出一張《部分匹配表》(Partial Match Table)票从。這張表是如何產(chǎn)生的漫雕,后面再介紹滨嘱,這里只要會(huì)用就可以了。


9.如下圖所示浸间,已知空格與D不匹配時(shí)太雨,前面六個(gè)字符”ABCDAB”是匹配的。查表可知魁蒜,”ABCDAB”中最后一個(gè)匹配字符B對(duì)應(yīng)的”部分匹配值”為2囊扳,因此按照下面的公式算出向后移動(dòng)的位數(shù):
 移動(dòng)位數(shù) = 已匹配的字符數(shù) - 對(duì)應(yīng)的部分匹配值

因?yàn)?6 - 2 等于4,所以將模式串整體向后移動(dòng)4位兜看。
10.如下圖锥咸,因?yàn)榭崭衽cC不匹配,模式串還要繼續(xù)往后移细移。這時(shí)搏予,已匹配的字符數(shù)為2(”AB”),對(duì)應(yīng)的”部分匹配值”為0弧轧。所以雪侥,移動(dòng)位數(shù) = 2 - 0,結(jié)果為 2精绎,于是將模式串向后移2位速缨。


11.如下圖所示,因?yàn)榭崭衽cA不匹配代乃,需要繼續(xù)后移一位鸟廓。


12.逐位比較,直到發(fā)現(xiàn)C與D不匹配襟己。于是,移動(dòng)位數(shù) = 6 - 2牍陌,繼續(xù)將模式串向后移動(dòng)4位擎浴。


13.逐位比較,直到模式串的最后一位毒涧,發(fā)現(xiàn)完全匹配贮预,于是搜索完成。如果還要繼續(xù)搜索(即找出全部匹配)契讲,移動(dòng)位數(shù) = 7 - 0仿吞,再將模式串向后移動(dòng)7位,這里就不再重復(fù)了捡偏。

部分匹配表

下面介紹《部分匹配表》是如何產(chǎn)生的唤冈。


首先,要了解兩個(gè)概念:”前綴”和”后綴”银伟。 “前綴”指除了最后一個(gè)字符以外你虹,一個(gè)字符串的全部頭部組合绘搞;”后綴”指除了第一個(gè)字符以外,一個(gè)字符串的全部尾部組合傅物。

“部分匹配值”就是”前綴”和”后綴”的最長(zhǎng)的共有元素的長(zhǎng)度夯辖。以”ABCDABD”為例,

- "A"的前綴和后綴都為空集董饰,共有元素的長(zhǎng)度為0蒿褂;
- "AB"的前綴為[A],后綴為[B]卒暂,共有元素的長(zhǎng)度為0啄栓;
-  "ABC"的前綴為[A, AB],后綴為[BC, C]介却,共有元素的長(zhǎng)度0谴供;
-  "ABCD"的前綴為[A, AB, ABC],后綴為[BCD, CD, D]齿坷,共有元素的長(zhǎng)度為0桂肌;
-  "ABCDA"的前綴為[A, AB, ABC, ABCD],后綴為[BCDA, CDA, DA, A]永淌,共有元素為"A"崎场,長(zhǎng)度為1;
-  "ABCDAB"的前綴為[A, AB, ABC, ABCD, ABCDA]遂蛀,后綴為[BCDAB, CDAB, DAB, AB, B]谭跨,共有元素為"AB",長(zhǎng)度為2李滴;
-  "ABCDABD"的前綴為[A, AB, ABC, ABCD, ABCDA, ABCDAB]螃宙,后綴為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長(zhǎng)度為0所坯。

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


“部分匹配值”的實(shí)質(zhì)是,有時(shí)候芹助,字符串頭部和尾部會(huì)有重復(fù)堂湖。比如,”ABCDAB”之中有兩個(gè)”AB”状土,那么它的”部分匹配值”就是2(”AB”的長(zhǎng)度)无蜂。模式串移動(dòng)的時(shí)候,第一個(gè)”AB”向后移動(dòng)4位(字符串長(zhǎng)度-部分匹配值)蒙谓,就可以來(lái)到第二個(gè)”AB”的位置斥季。

理解next數(shù)組

理解KMP算法的核心和難點(diǎn)就是理解next數(shù)組的巧妙設(shè)計(jì),下面我會(huì)重點(diǎn)解釋next數(shù)組的含義累驮。
next數(shù)組泻肯,又叫做“失配函數(shù)”渊迁,它是以下標(biāo) 0 開(kāi)始的數(shù)組,為了方便大家理解灶挟,給出如下圖示:


根據(jù) KMP 算法琉朽,在失配位會(huì)調(diào)用該位的 next 數(shù)組的值,下面我將詳細(xì)道出next數(shù)組的來(lái)龍去脈稚铣。

next[i]表示在失配位i之前的最長(zhǎng)公共前后綴的長(zhǎng)度箱叁。

首先,我們?nèi)≈耙呀?jīng)匹配的部分(即藍(lán)色的那部分L枰健)


我們?cè)谏厦嬲f(shuō)到“最長(zhǎng)公共前后綴”耕漱,體現(xiàn)到下圖所示的樣子。


next數(shù)組的作用是通過(guò)尋找最長(zhǎng)公共前后綴的部分抬伺,快速移動(dòng)模式串螟够,從而提高字符串匹配的效率,如下圖所示:


next[i]返回當(dāng)前位置i的最長(zhǎng)公共前后綴的長(zhǎng)度峡钓,假設(shè)為 len 妓笙。因?yàn)閿?shù)組是由 0 開(kāi)始的,所以 next 數(shù)組讓模式串的第 len 位與主串匹配就是拿最長(zhǎng)前綴之后的第 1 位與失配位重新匹配能岩,避免匹配串從頭開(kāi)始寞宫,如下圖所示。


如果上圖中的紅色位置依然匹配失效拉鹃,則需要對(duì)上圖中的綠色部分再次去求解它的最長(zhǎng)公共前后綴長(zhǎng)度(假設(shè)為len’)辈赋,然后繼續(xù)向右移動(dòng)模式串,讓模式串的第 len’ 位與主串的失配位重新進(jìn)行匹配膏燕,如果仍舊不匹配钥屈,則繼續(xù)以上過(guò)程操作。如下圖所示:


我們發(fā)現(xiàn)坝辫,當(dāng)發(fā)生失配的時(shí)候篷就,可以借助遞推的思想,根據(jù)已知的結(jié)果繼續(xù)求出當(dāng)前失配位之前的最長(zhǎng)公共前后綴的長(zhǎng)度阀溶,然后,繼續(xù)移動(dòng)模式串鸦泳,從而進(jìn)行新一輪的字符串匹配银锻。

解釋這么多,那么next數(shù)組究竟如何求出呢做鹰?

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


1.當(dāng)紅色部分相同(即S[k]==S[q])時(shí),則當(dāng)前 next 數(shù)組的值為上一次 next 的值加一(即next[q] = k++)钾麸,如上圖所示更振。

2.當(dāng)紅色部分不等的時(shí)候炕桨,則需要對(duì)綠色部分遞推求解 k’ = next[k-1],然后再對(duì)新的 k’ 位置字符與 q 位置字符進(jìn)行匹配肯腕,如果相等献宫,則 next[q] = k’+1,否則实撒,執(zhí)行遞推匹配姊途,直到k’=0時(shí)遞推結(jié)束。比如知态,模式串“ABCABXABCABC”捷兰,最后一個(gè)字符C的next數(shù)組值為3。(因?yàn)镃之前的最長(zhǎng)公共前后綴為“ABCAB”负敏,而“ABCAB”的最長(zhǎng)公共前后綴為“AB”贡茅,其長(zhǎng)度為2,又源于第三個(gè)字符C與最后一個(gè)字符C匹配其做,所以最后一個(gè)字符C的next數(shù)組值為3)

代碼實(shí)現(xiàn)

創(chuàng)建文件kmp.c顶考,內(nèi)容如下:

#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數(shù)組:");    
    for (i = 0; i < strlen(P); ++i)
    {        
        printf("%d ",next[i]);
    }    
    printf("\n");    
    return 0;
}

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

$ gcc -o kmp kmp.c
$ ./kmp
# 其運(yùn)行結(jié)果如下:

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

總結(jié)

理解KMP算法的難點(diǎn)就在于理解next數(shù)組的實(shí)現(xiàn)庶柿,在遇到失配位時(shí)能夠靈活地應(yīng)用遞推方法村怪,根據(jù)已知的結(jié)果,進(jìn)一步求解出子最長(zhǎng)公共前后綴的長(zhǎng)度浮庐,然后進(jìn)一步的完成新一輪的匹配甚负,從而避免從頭開(kāi)始,極大提高了匹配速率审残。kmp算法的時(shí)間復(fù)雜度O(n+m)梭域,可以采用均攤分析來(lái)解答,具體可參考算法導(dǎo)論搅轿。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末病涨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子璧坟,更是在濱河造成了極大的恐慌既穆,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雀鹃,死亡現(xiàn)場(chǎng)離奇詭異幻工,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)黎茎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)囊颅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事踢代∶ぴ鳎” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵胳挎,是天一觀(guān)的道長(zhǎng)饼疙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)串远,這世上最難降的妖魔是什么宏多? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮澡罚,結(jié)果婚禮上伸但,老公的妹妹穿的比我還像新娘。我一直安慰自己留搔,他們只是感情好更胖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著隔显,像睡著了一般却妨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上括眠,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天彪标,我揣著相機(jī)與錄音,去河邊找鬼掷豺。 笑死捞烟,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的当船。 我是一名探鬼主播题画,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼德频!你這毒婦竟也來(lái)了苍息?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤壹置,失蹤者是張志新(化名)和其女友劉穎竞思,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體钞护,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡盖喷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了患亿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片传蹈。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖步藕,靈堂內(nèi)的尸體忽然破棺而出惦界,到底是詐尸還是另有隱情,我是刑警寧澤咙冗,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布沾歪,位于F島的核電站,受9級(jí)特大地震影響雾消,放射性物質(zhì)發(fā)生泄漏灾搏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一立润、第九天 我趴在偏房一處隱蔽的房頂上張望狂窑。 院中可真熱鬧,春花似錦桑腮、人聲如沸泉哈。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)丛晦。三九已至,卻和暖如春提陶,著一層夾襖步出監(jiān)牢的瞬間烫沙,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工隙笆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锌蓄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓仲器,卻偏偏與公主長(zhǎng)得像煤率,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乏冀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 引言 字符串匹配一直是計(jì)算機(jī)科學(xué)領(lǐng)域研究和應(yīng)用的熱門(mén)領(lǐng)域蝶糯,算法的改進(jìn)研究一直是一個(gè)十分困難的課題。作為字符串匹配中...
    潮汐行者閱讀 1,639評(píng)論 2 6
  • 數(shù)據(jù)結(jié)構(gòu)與算法--KMP算法查找子字符串 部分內(nèi)容和圖片來(lái)自這三篇文章: 這篇文章辆沦、這篇文章昼捍、還有這篇他們寫(xiě)得非常...
    sunhaiyu閱讀 1,737評(píng)論 1 21
  • 在字符串系列的算法中,KMP算法屬于較難的一個(gè)肢扯。實(shí)際上它的代碼并不多妒茬,主要一些細(xì)節(jié)的地方難以理解,再加上書(shū)上蔚晨,網(wǎng)上...
    zero_sr閱讀 1,728評(píng)論 0 9
  • 20歲。但是我剛出生银择《嗫罚可能我也已死去。
    超黃的高尚閱讀 186評(píng)論 0 0
  • 【原文】 《菜根譚》抱樸守拙 涉世之道涉世淺浩考,點(diǎn)染亦淺夹孔;歷事深,機(jī)械亦深析孽。故君子與其練達(dá)搭伤,不若樸魯;與其曲謹(jǐn)袜瞬,不若...
    沂河生閱讀 543評(píng)論 2 5