KMP算法及優(yōu)化

轉(zhuǎn)載請(qǐng)注明出處: KMP算法及優(yōu)化

今天看到同學(xué)在復(fù)習(xí)數(shù)據(jù)結(jié)構(gòu)書上的KMP算法遣疯,忽然發(fā)覺自己又把KMP算法忘掉了,以前就已經(jīng)忘過(guò)一次,看樣子還是沒(méi)有真正的掌握它,這回學(xué)聰明點(diǎn)柑晒,再次搞明白后記錄下來(lái)。


一般字符串匹配過(guò)程

KMP算法是字符串匹配算法的一種改進(jìn)版眷射,一般的字符串匹配算法是:從主串(目標(biāo)字符串)模式串(待匹配字符串)的第一個(gè)字符開始比較匙赞,如果相等則繼續(xù)匹配下一個(gè)字符, 如果不相等則從主串的下一個(gè)字符開始匹配妖碉,直到模式串被匹配完涌庭,則匹配成功,或主串被匹配完且模式串未匹配完欧宜,則匹配失敗坐榆。匹配過(guò)程入下圖:

一般匹配方法.PNG

這種實(shí)現(xiàn)方式是最簡(jiǎn)單的, 但也是低效的冗茸,因?yàn)榈谌纹ヅ浣Y(jié)束后的第四次和第五次是沒(méi)有必要的席镀。

分析

第三次匹配在j = 0(a)i = 2(a)處開始羹铅,在j = 4(c)i = 6(b)處失敗,這意味著模式串和主串中:j = 0(a)i = 2(a)愉昆、j = 1(b)i = 3(b)j = 2(c)i = 4(c)麻蹋、j = 3(a)i = 5(a)這四個(gè)字符相互匹配跛溉。

分析模式串的前3個(gè)字符:模式串的第一個(gè)字符j = 0是aj = 1(b)扮授、j = 2(c)這兩個(gè)字符和j = 0(a)不同芳室,因此以這兩個(gè)字符開頭的匹配必定失敗,在第三次匹配中刹勃,主串中i = 3(b)堪侯、i = 4(c)和模式串j = 1(b)j = 2(c)相互匹配荔仁,因此匹配失敗后伍宦,可以直接跳過(guò)主串中i = 3(b)i = 4(c)這兩個(gè)字符的匹配乏梁。

繼續(xù)分析模式串的j = 3(a)j = 4(c)這兩個(gè)字符次洼,如果模式串匹配到j = 4(c)這個(gè)字符才失敗的話,因?yàn)?code>j = 4(c)的前一個(gè)字符j = 3(a)和第一個(gè)字符j = 0(a)是相同的遇骑,結(jié)合上一個(gè)分析得知:

1):下一次匹配中主串已經(jīng)跳過(guò)了和j = 3(a)前兩個(gè)相互匹配的字符i = 3(b)卖毁、i = 4(c),將從i = 5(a)開始匹配落萎。
2):j = 3(a)i = 5(a)相互匹配亥啦。

因此下一次匹配認(rèn)為j = 3(a)i = 5(a)已經(jīng)匹配過(guò)了,匹配從j = 4(b)i = 6(b)開始练链,這樣的話也跳過(guò)了j = 3(a)這個(gè)字符的匹配翔脱。

同理可得第二次匹配也是沒(méi)必要的。

KMP算法

KMP算法匹配過(guò)程

利用KMP算法匹配的過(guò)程如下圖:

KMP算法匹配過(guò)程.PNG

KMP算法的改進(jìn)之處在于:能夠知道在匹配失敗后兑宇,有多少字符是不需要進(jìn)行匹配可以直接跳過(guò)的碍侦,匹配失敗后,下一次匹配從什么地方開始能夠有效的減少不必要的匹配過(guò)程隶糕。

next[n]求解方法

由上面的分析可以發(fā)現(xiàn)瓷产,KMP算法的核心在于對(duì)模式串本身的分析,其分析結(jié)果能提供在j = n位置匹配失敗時(shí)枚驻,從j = 0j = n - 1這個(gè)子串中前綴和后綴的最長(zhǎng)公共匹配的字符數(shù)濒旦,這樣說(shuō)可能比較難以理解,看下圖:

最長(zhǎng)公共匹配字符數(shù).PNG

在得到子串前綴和后綴的最長(zhǎng)公共匹配字符數(shù)l后再登,以后在i = x,j = n處匹配失敗時(shí)尔邓,可以直接從i = x,j = l處繼續(xù)匹配(證明過(guò)程參考:嚴(yán)蔚敏的《數(shù)據(jù)結(jié)構(gòu)》4.3章)晾剖,這樣問(wèn)題就很明顯了,我們要求出n和l對(duì)應(yīng)的值梯嗽,其中n是模式串字符數(shù)組的下標(biāo)齿尽,l的有序集合通常稱之為next數(shù)組,前面兩個(gè)模式串的next數(shù)組下標(biāo)n的對(duì)應(yīng)如下:

next[l].PNG

模式串2完整匹配過(guò)程

有了這個(gè)next數(shù)組灯节,那么在匹配的過(guò)程中我們就能在j = n處匹配失敗后循头,根據(jù)next[n]的值進(jìn)行偏移,其中next[0]固定為-1炎疆,代表在當(dāng)前i這個(gè)位置整個(gè)模式串和主串都無(wú)法匹配成功卡骂,要從下一個(gè)位置i = i + 1j = 0處開始匹配,模式串2的匹配過(guò)程如下:

模式串2匹配過(guò)程.PNG

現(xiàn)在知道了next數(shù)組的作用形入,也知道在有next數(shù)組時(shí)的匹配過(guò)程全跨,那么剩下的問(wèn)題就是如何通過(guò)代碼求出next數(shù)組匹配過(guò)程了。

next數(shù)組的過(guò)程可以認(rèn)為是將模式串拆分成n個(gè)子串亿遂,分別對(duì)每個(gè)子串求前綴和后綴的最長(zhǎng)公共匹配字符數(shù)l浓若,這一點(diǎn)可以通過(guò)上圖(最長(zhǎng)公共匹配字符數(shù))看出來(lái)(沒(méi)有畫出l=0時(shí)的圖解)看出來(lái)。

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

next數(shù)組的代碼如下:

void get_next(string pattern, int next[]) {
    int i = 0; // i用來(lái)記錄當(dāng)前計(jì)算的next數(shù)組元素的下標(biāo)崩掘, 同時(shí)也作為模式串本身被匹配到的位置的下標(biāo)
    int j = 0; // j == -1 代表從在i的位置模式串無(wú)法匹配成功七嫌,從下一個(gè)位置開始匹配
    next[0] = -1; // next[0]固定為-1
    int p_len = pattern.length();
    while (++i < p_len) {
        if (pattern[i] == pattern[j]) {
            // j是用來(lái)記錄當(dāng)前模式串匹配到的位置的下標(biāo), 這就意味著當(dāng)j = l時(shí)苞慢,
            // 則在pattern[j]這個(gè)字符前面已經(jīng)有l(wèi) - 1個(gè)成功匹配,
            // 即子串前綴和后綴的最長(zhǎng)公共匹配字符數(shù)有l(wèi) - 1個(gè)诵原。
            next[i] = j++;
        } else {
            next[i] = j;
            j = 0;
            if (pattern[i] == pattern[j]) {
                j++;
            }
        }
    }
}

根據(jù)next數(shù)組求模式串在主串中的位置代碼如下:

int search(string source, string pattern, int next[]) {
    int i = 0;
    int j = 0;
    int p_len = pattern.length();
    int s_len = source.length();
    while (j < p_len && i < s_len) {
        if (j == -1 || source[i] == pattern[j]) {
            i++;
            j++;
        }
        else {
            j = next[j];            
        }
    }
    if (j < pattern.length())
        return -1;
    else
        return i - pattern.length();
}

測(cè)試代碼如下:

int main() {
    string source = "ABCDABCEAAAABASABCDABCADABCDABCEAABCDABCEAAABASABCDABCAABLAKABCDABABCDABCEAAADSFDABCADABCDABCEAAABCDABCEAAABASABCDABCADABCDABCEAAABLAKABLAKK";
    // string pattern = "abcaaabcab";
    string pattern = "ABCDABCEAAABASABCDABCADABCDABCEAAABLAK";
    int next[pattern.length()] = { NULL };
    get_next(pattern, next);
    cout << "next數(shù)組: \t";
    for (int i = 0; i < pattern.length(); i++)
        cout << next[i] << " ";
    cout << endl;
    int pos = search(source, pattern, next);
    if (-1 != pos) {
        cout << "匹配成功,模式串在主串中首次出現(xiàn)的位置是: 第" << pos + 1 << "位";
        getchar();
        return 0;
    } else {
        cout << "匹配失敗";
    }
    getchar();
    return 0;
}

執(zhí)行結(jié)果:

next數(shù)組: -1 0 0 0 0 1 2 3 0 1 1 1 2 1 0 1 2 3 4 5 6 7 1 0 1 2 3 4 5 6 7 8 9 10 11 12 0 1 
匹配成功挽放,模式串在主串中首次出現(xiàn)的位置是: 第97位

KMP算法優(yōu)化

再回過(guò)頭去看模式串2的next數(shù)組的圖:

模式串2的next[n].PNG

如果模式串和主串的匹配在j = 6(b)處失敗的話绍赛,根據(jù)j = next[6] = 1得知下一次匹配從j = 1處開始,j = 1處的字符和j = 6處的字符同為c辑畦,因此這次匹配必定會(huì)失敗吗蚌。
同樣的,模式串和主串的匹配在j = 7(c)處或在j = 9(b)處失敗的話纯出,根據(jù)next數(shù)組偏移后下一次匹配也必定會(huì)失敗蚯妇。

考慮如果模式串是: aaaac,根據(jù)一般的KMP算法求出的next數(shù)組及匹配過(guò)程如下:

KMP算法無(wú)效匹配過(guò)程.PNG

顯而易見暂筝,在第二次匹配失敗后箩言,第三、四焕襟、五次匹配都是沒(méi)有意義的陨收,j = next[3]、j = next[2]鸵赖、j = next[1]务漩、j = next[0]這四處的字符都是a拄衰,在j = 3(a)處匹配失敗時(shí),根據(jù)模式串本身就應(yīng)該可以得出結(jié)論:可以跳過(guò)j = 2(a)饵骨、j = 1(a)翘悉、j = 0(a)的匹配,直接從i = i + 1 居触、j = 0處開始匹配镐确,所以優(yōu)化過(guò)后的next數(shù)組應(yīng)該是:

優(yōu)化過(guò)后的next數(shù)組.PNG

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

優(yōu)化后的求next數(shù)組的代碼如下:

void get_next(string pattern, int next[]) {
    int i = 0; // i用來(lái)記錄當(dāng)前計(jì)算的next數(shù)組元素的下標(biāo), 同時(shí)也作為模式串本身被匹配到的位置的下標(biāo)
    int j = 0; // j == -1 代表從在i的位置模式串無(wú)法匹配成功饼煞,從下一個(gè)位置開始匹配
    next[0] = -1; // next[0]固定為-1
    int p_len = pattern.length();
    while (++i < p_len) {
        if (pattern[i] == pattern[j]) {
            // j是用來(lái)記錄當(dāng)前模式串匹配到的位置的下標(biāo), 這就意味著當(dāng)j = l時(shí)诗越,
            // 則在pattern[j]這個(gè)字符前面已經(jīng)有l(wèi) - 1個(gè)成功匹配,
            // 即子串前綴和后綴的最長(zhǎng)公共匹配字符數(shù)有l(wèi) - 1個(gè)砖瞧。
            next[i] = j++;
            
            // 當(dāng)根據(jù)next[i]偏移后的字符與偏移前的字符向同時(shí)
            // 那么這次的偏移是沒(méi)有意義的,因?yàn)槠ヅ浔囟〞?huì)失敗
            // 所以可以一直往前偏移嚷狞,直到
            // 1): 偏移前的字符和偏移后的字符不相同块促。
            // 2): next[i] == -1
            while (next[i] != -1 && pattern[i] == pattern[next[i]]) {
                next[i] = next[next[i]];
            }
        } else {
            next[i] = j;
            j = 0;
            if (pattern[i] == pattern[j]) {
                j++;
            }
        }
    }
}

結(jié)尾

希望本文能對(duì)你有幫助, 如果有什么問(wèn)題床未, 歡迎探討竭翠。

參考文獻(xiàn)

嚴(yán)蔚敏的《數(shù)據(jù)結(jié)構(gòu)》4.3章
kmp算法--百度百科

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市薇搁,隨后出現(xiàn)的幾起案子斋扰,更是在濱河造成了極大的恐慌,老刑警劉巖啃洋,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件传货,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡宏娄,警方通過(guò)查閱死者的電腦和手機(jī)问裕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)孵坚,“玉大人粮宛,你說(shuō)我怎么就攤上這事÷舫瑁” “怎么了巍杈?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)逗堵。 經(jīng)常有香客問(wèn)我秉氧,道長(zhǎng),這世上最難降的妖魔是什么蜒秤? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任汁咏,我火速辦了婚禮亚斋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘攘滩。我一直安慰自己帅刊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布漂问。 她就那樣靜靜地躺著赖瞒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蚤假。 梳的紋絲不亂的頭發(fā)上栏饮,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音磷仰,去河邊找鬼袍嬉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛灶平,可吹牛的內(nèi)容都是我干的伺通。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼逢享,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼罐监!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起瞒爬,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤弓柱,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后侧但,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吆你,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年俊犯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了妇多。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡燕侠,死狀恐怖者祖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绢彤,我是刑警寧澤七问,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站茫舶,受9級(jí)特大地震影響械巡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一讥耗、第九天 我趴在偏房一處隱蔽的房頂上張望有勾。 院中可真熱鬧,春花似錦古程、人聲如沸蔼卡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)雇逞。三九已至,卻和暖如春茁裙,著一層夾襖步出監(jiān)牢的瞬間塘砸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工晤锥, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谣蠢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓查近,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親挤忙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子霜威,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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