KMP算法理解

KMP的由來(lái)

在KMP算法之前,對(duì)文本進(jìn)行匹配時(shí)使用的是樸素模式匹配算法,也就是最簡(jiǎn)單匹配算法.
當(dāng)然運(yùn)行效率也是讓人深惡痛絕,舉個(gè)例子:

現(xiàn)有長(zhǎng)度為n的模式串00001,和長(zhǎng)度為m的文本串0000000000000000000000000000000001.
按照樸素匹配算法最終時(shí)間復(fù)雜度為o{(m-n+1)*n}

這種有多個(gè)0和1重復(fù)字符的字符串,匹配模式卻仍需要挨個(gè)遍歷,這是十分糟糕的,所以最終KMP算法誕生了.

KMP和樸素算法的核心差別

樸素算法在匹配時(shí),子串和對(duì)比的目標(biāo)串都會(huì)不斷的進(jìn)行回溯對(duì)比,而KMP會(huì)先計(jì)算出子串的匹配數(shù)組,在進(jìn)行匹配時(shí)目標(biāo)串并不會(huì)進(jìn)行回溯,且子串回溯時(shí)會(huì)根據(jù)計(jì)算出的重復(fù)數(shù)組省略很多不必要的回溯.
下面就用例子進(jìn)行講解.

KMP的回溯原理

引用阮一峰:字符串匹配的KMP算法

以下圖兩字符串匹配為例:

未編譯

KMP核心是找出要匹配的子串的匹配值數(shù)組,如下圖給出的子串和匹配值.

未編譯

"子串匹配值"就是"前綴"和"后綴"的最長(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。

跳過(guò)前面一端不匹配的,我們直接來(lái)到匹配串生效的位置

已知空格與D不匹配時(shí)往史,前面六個(gè)字符"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位。

因?yàn)榭崭衽cC不匹配订歪,搜索詞還要繼續(xù)往后移脖祈。這時(shí),已匹配的字符數(shù)為2("AB")刷晋,對(duì)應(yīng)的"部分匹配值"為0盖高。所以,移動(dòng)位數(shù) = 2 - 0眼虱,結(jié)果為 2或舞,于是將搜索詞向后移2位。

因?yàn)榭崭衽cA不匹配蒙幻,繼續(xù)后移一位映凳。

逐位比較,直到發(fā)現(xiàn)C與D不匹配邮破。于是诈豌,移動(dòng)位數(shù) = 6 - 2,繼續(xù)將搜索詞向后移動(dòng)4位抒和。

逐位比較矫渔,直到搜索詞的最后一位,發(fā)現(xiàn)完全匹配摧莽,于是搜索完成庙洼。如果還要繼續(xù)搜索(即找出全部匹配),移動(dòng)位數(shù) = 7 - 0,再將搜索詞向后移動(dòng)7位油够,這里就不再重復(fù)了蚁袭。

這么一看KMP是不是比樸素算法匹配次數(shù)少了很多?例子中也看出了KMP核心在于匹配數(shù)組,那如何通過(guò)代碼求出匹配數(shù)組呢?

KMP核心匹配串?dāng)?shù)組

下面是Java的偽代碼


    public int[] get_next(char[] str) {
        int i = 1;
        int j = 0;
        int[] next = new int[str.length];
        next[0] = 0;
        while (i < str.length) {
            if (str[i]==(str[j])) { // 匹配記錄下當(dāng)前匹配的位置
                ++j;
                next[i] = j;  // 為了配合例子更便于理解,所以在++i之前便給匹配數(shù)組進(jìn)行了賦值
                ++i;
            } else if (j == 0) {  // 完全不匹配,跳至下一個(gè)字符
                next[i] = j;  // 同上理由
                ++j;
                ++i;
            } else {
                j = next[j - 1];  // 回溯至合適的位置
            }
        }
        return next;
    }

這段代碼中j = next[j - 1];是很多人懵逼的地方,當(dāng)然常規(guī)模式是j = next[j];.我這里配合例子進(jìn)行了改寫.
為什么不是遞減回溯,而是直接跳至數(shù)組中的匹配值,盡管從結(jié)果中可以看出這樣確實(shí)是高效的方式,但是為什么正好就是最合適呢?知其然不知其所以然.

下面按照上面思路來(lái)實(shí)現(xiàn)KMP算法就能知其所以然了.

KMP算法具體實(shí)現(xiàn)

    // 求字符串t,在字符串s中的pos位之后首次出現(xiàn)的位置
    public int index_KMP(char[] s, char[] t, int pos) {
        int i = pos;
        int j = 0;
        int[] next = get_next(t);
        while (i < s.length && j < t.length) {
            if (s[i] == t[j]) {
                i++;
                j++;
            } else if (j == 0) {
                i++;
            } else {
                // j的回溯位置可以根據(jù)上面例子中的公式來(lái)套.
                // 移動(dòng)位數(shù) = 已匹配的字符數(shù) - 對(duì)應(yīng)的部分匹配值;
                // 已匹配的字符數(shù)為: j;
                // 對(duì)應(yīng)部分匹配值為: next[j - 1];
                // 移動(dòng)位數(shù)為: j - (next[j - 1]);
                // 新的索引 = 舊索引 - 移動(dòng)位數(shù);
                // j = j - (j - (next[j - 1]));
                j = next[j - 1];
            }
        }
        if (j == t.length) {
            return i - t.length;
        }
        return 0;
    }

從這里就可以看出為什么回溯的位置是j = next[j - 1],在于常規(guī)的對(duì)應(yīng)一下,常規(guī)的kmp算法數(shù)組中當(dāng)前位對(duì)應(yīng)的是前一位的重復(fù)值,并不是本身的重復(fù)值.
在上述公式中我們需要找到前一位的對(duì)應(yīng)值,來(lái)進(jìn)行位移操作,而常規(guī)KMP中前一位重復(fù)值就是next[j].
所以最終的回縮過(guò)程就是 j = next[j];

便于理解,在求自身匹配數(shù)組時(shí),也可以將不斷變換的尾串看做成子串,這樣就是子串和目標(biāo)串進(jìn)行匹配,就可以套用上面的公式得出j = next[j],只不過(guò)尾串一直在變化而已.

在改進(jìn)KMP算法時(shí),需先將算法恢復(fù)到正常模式(想了半天不知道在改進(jìn)模式的情況下如何繼續(xù)推導(dǎo)了...)
常規(guī)的KMP匹配數(shù)組獲取代碼如下:

    // 由于數(shù)組的角標(biāo)是從0開(kāi)始的,所以的出來(lái)的結(jié)果和書上所說(shuō)的統(tǒng)統(tǒng)小一.
    public int[] get_next(char[] str) {
        int i = 0;
        int j = -1;
        int[] nextval = new int[str.length];
        nextval[0] = -1;
        while (i < str.length - 1) {
            if (j == -1 || str[i] == (str[j])) {   // 匹配記錄下當(dāng)前匹配的位置
                ++j;
                ++i;
                nextval[i] = j;
            } else {
                j = nextval[j];  // 回溯至合適的位置
            }
        }
        return nextval;
    }

常規(guī)模式KMP匹配代碼如下:

    public int index_KMP(char[] s, char[] t, int pos) {
        int i = pos;
        int j = 0;
        int[] next = get_next(t);
        while (i < s.length && j < t.length) {
            if (j == 0 || s[i] == t[j]) {
                i++;
                j++;
            } else {
                j = next[j];
            }
        }
        if (j == t.length) {
            return i - t.length;
        }
        return 0;
    }

KMP算法的改進(jìn)

引自July:從頭到尾徹底理解KMP(2014年8月22日版)
用前面的next 數(shù)組方法求模式串“abab”的next 數(shù)組,可得其next 數(shù)組為-1 0 0 1(0 0 1 2整體右移一位石咬,初值賦為-1)揩悄,當(dāng)它跟下圖中的文本串去匹配的時(shí)候,發(fā)現(xiàn)b跟c失配鬼悠,于是模式串右移j - next[j] = 3 - 1 =2位删性。

P表子串(abab),s表目標(biāo)串(abacabababc)
右移2位后,b又跟c失配焕窝。事實(shí)上蹬挺,因?yàn)樵谏弦徊降钠ヅ渲校呀?jīng)得知p[3] = b它掂,與s[3] = c失配汗侵,而右移兩位之后,讓p[next[3]] = p[1] = b 再跟s[3]匹配時(shí)群发,必然失配晰韵。問(wèn)題出在哪呢?

問(wèn)題出在不該出現(xiàn)p[j] = p[next[j]]熟妓。為什么呢雪猪?理由是:當(dāng)p[j] != s[i] 時(shí),下次匹配必然是p[next[j]] 跟s[i]匹配起愈,如果p[j] = p[next[j]]只恨,必然導(dǎo)致后一步匹配失敗(因?yàn)閜[j]已經(jīng)跟s[i]失配抬虽,然后你還用跟p[j]等同的值p[next[j]]去跟s[i]匹配官觅,很顯然,必然失配)阐污,所以不能允許p[j] = p[next[j]]休涤。如果出現(xiàn)了p[j] = p[next[j] ]咋辦呢?如果出現(xiàn)了笛辟,則需要再次遞歸功氨,即令next[j] = next[next[j]]。

    public int[] get_nextval(char[] str) {
        int i = 0;
        int j = -1;
        int[] nextval = new int[str.length];
        nextval[0] = -1;
        while (i < str.length - 1) {
            if (j == -1 || str[i] == (str[j])) {   // 匹配記錄下當(dāng)前匹配的位置;
                ++j;
                ++i;
                nextval[i] = j;
                if (str[i] != str[j]) { // 當(dāng)前字符與前綴字符不同;
                    nextval[i] = j;  // 當(dāng)前的j賦值給nextval[i];
                } else {
                    nextval[i] = nextval[j]; // 與前綴字符相同,那么將前綴字符的nextval值付給在i位的值.
                }
            } else {
                j = nextval[j];  // 回溯至合適的位置
            }
        }
        return nextval;
    }

本文到此結(jié)束.
前面KMP推導(dǎo)算是個(gè)人理解,后面的改進(jìn)照搬July的,因?yàn)閷?shí)在說(shuō)的很清楚了,沒(méi)有能補(bǔ)充的,若還是有點(diǎn)蒙圈,可以看July原文,或許更容易理解.

參考博客

阮一峰:字符串匹配的KMP算法
July:從頭到尾徹底理解KMP(2014年8月22日版)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末手幢,一起剝皮案震驚了整個(gè)濱河市捷凄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌围来,老刑警劉巖跺涤,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匈睁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡桶错,警方通過(guò)查閱死者的電腦和手機(jī)航唆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)牛曹,“玉大人佛点,你說(shuō)我怎么就攤上這事醇滥±璞龋” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵鸳玩,是天一觀的道長(zhǎng)阅虫。 經(jīng)常有香客問(wèn)我,道長(zhǎng)不跟,這世上最難降的妖魔是什么颓帝? 我笑而不...
    開(kāi)封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮窝革,結(jié)果婚禮上购城,老公的妹妹穿的比我還像新娘。我一直安慰自己虐译,他們只是感情好瘪板,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著漆诽,像睡著了一般侮攀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上厢拭,一...
    開(kāi)封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天兰英,我揣著相機(jī)與錄音,去河邊找鬼供鸠。 笑死畦贸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的楞捂。 我是一名探鬼主播家制,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼泡一!你這毒婦竟也來(lái)了颤殴?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鼻忠,失蹤者是張志新(化名)和其女友劉穎涵但,沒(méi)想到半個(gè)月后杈绸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡矮瘟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年瞳脓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澈侠。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡劫侧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出哨啃,到底是詐尸還是另有隱情烧栋,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布拳球,位于F島的核電站审姓,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏祝峻。R本人自食惡果不足惜魔吐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望莱找。 院中可真熱鬧酬姆,春花似錦、人聲如沸奥溺。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)谚赎。三九已至淫僻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間壶唤,已是汗流浹背雳灵。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闸盔,地道東北人悯辙。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像迎吵,于是被迫代替她去往敵國(guó)和親躲撰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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

  • 文章大綱:1.KMP算法概念2.KMP算法中最核心的next[] 數(shù)組是如何生成的3.使用KMP算法 匹配字符串 ...
    檸檬烏冬面閱讀 815評(píng)論 0 3
  • 數(shù)據(jù)結(jié)構(gòu)與算法--KMP算法查找子字符串 部分內(nèi)容和圖片來(lái)自這三篇文章: 這篇文章击费、這篇文章拢蛋、還有這篇他們寫得非常...
    sunhaiyu閱讀 1,735評(píng)論 1 21
  • 數(shù)據(jù)結(jié)構(gòu) 第8講 KMP算法 講這個(gè)算法之前,我們首先了解幾個(gè)概念: 串:又稱字符串蔫巩,是由零個(gè)或多個(gè)字符組成的有限...
    rainchxy閱讀 1,294評(píng)論 0 3
  • 引言 字符串匹配一直是計(jì)算機(jī)科學(xué)領(lǐng)域研究和應(yīng)用的熱門領(lǐng)域谆棱,算法的改進(jìn)研究一直是一個(gè)十分困難的課題快压。作為字符串匹配中...
    潮汐行者閱讀 1,639評(píng)論 2 6
  • 最初想寫下這篇觀后感時(shí)的心情和此刻我下筆的心情完全不一樣。 把第七季養(yǎng)肥后垃瞧,我開(kāi)始關(guān)注這部劇的花邊蔫劣,一些劇情,例如...
    慕傾所向閱讀 534評(píng)論 0 3