字符串匹配--KMP算法

字符串匹配(查找)算法是一類重要的字符串算法(String Algorithm)。有兩個字符串, 長度為m的haystack(查找串)和長度為n的needle(模式串), 它們構(gòu)造自同一個有限的字母表(Alphabet)。如果在haystack中存在一個與needle相等的子串,返回子串的起始下標,否則返回-1。C/C++、PHP中的strstr函數(shù)實現(xiàn)的就是這一功能。LeetCode上也有類似的題目,比如#28#187.

關(guān)于KMP,網(wǎng)上有很多很好的資料了,但我看了好久才看基本明白這到底是個什么玩意。于是決定自己總結(jié)一下,在這里做個筆記。
Knuth-Morris-Pratt算法,簡稱KMP,是一個經(jīng)典的字符串查找算法。Knuth就是傳說中的Donald Knuth,著有[The Art of Computer Programming](The Art of Computer Programming)。
我們知道,在進行字符串查找時,應當盡量避免不必要的匹配,以此來提高查找效率。因此Boyer-Moore、Sunday、KMP等算法,都試著在發(fā)生失配時,利用模式串或查找串的某種信息來跳過一些無意義的匹配嘗試。KMP關(guān)注的是模式串的部分匹配信息,這種信息是獨立于查找串之外的。
先來看個栗子,圖中上面的串是haystack,下面的則是needle。


在上圖中的查找過程中,當haystack[5]=c,needle[5]=d時發(fā)生失配了。接下來當然要把needle往右移動啦,最Naive辦法是只移動一位,而這必然是低效的,圖樣圖森破。
我們先來觀察一下這個needle串:

在發(fā)生失配的字母d的左邊,右一個子串needle[3...4]=ab,在needle的最開始,也有一個子串needle[0...1]=ab。我們直到d才發(fā)現(xiàn)失配,說明d之前的串都是能夠匹配的,所以haystack串中也有兩個ab。當我們右移needle時,還是要求在至少haystack的后續(xù)串中能找到一個ab#ab#,而如果這倆#恰好分別是c和d,就查找成功了。我想說的就是,這里d失配了,但是d之前的部分匹配串應該被利用起來。如果我們右移needle,使得左邊那個abhaystack[3...4]=ab對應,那我們就成功越過了很多個位置,而且不會有遺漏某個成功匹配的危險。
于是我們到了這個狀態(tài):

接著發(fā)現(xiàn)在haystack[7]=d的時候右失配了:

接著觀察needle,發(fā)現(xiàn)在needle[4]之前有一個needle[3]=a,在needle的開始處needle[0]也是a

于是我們右移needle,把needle[0]=a和haystack[6]=a對其:

繼續(xù)這個過程,直到haystack結(jié)束,或者找到一個完整的needle。
在上述過程中,對haystack的搜索是持續(xù)向右的,下標沒有任何回退。并且對于needle,我們利用部分匹配的串,將其游標往回倒了倒。注意,只是把下標往回倒,重新對齊。(注意這些左移右移什么鬼的,是相對的)
KMP算法正是利用這種部分匹配串的信息,來跳過不必要的匹配嘗試。KMP算法定義了一個數(shù)組,通常稱為next數(shù)組。next[i]是一個非負整數(shù),表示的意思是,如果在needle的位置i上發(fā)生失配,應該使needle的下標回到哪個位置。例如對于上面的needle串,當needle[5]=d發(fā)生失配,我們右移needle使得needle[0..1]和haystack[3...4]對齊。于是在下一輪查找中,我們可以直接從needle[2]=c開始匹配,也就是說,needle的下標由5回退到了2。所以對于此needle,其next[5]=2

對于KMP算法的理解,難點就在于next數(shù)組的計算。這里嘗試用自認為比較好理解的表述方式來寫一寫。

如前所述,next[i]表示當在needle[i]的位置發(fā)生失配時,下標i應該倒回的位置,同時也表示,在i的前一個位置,有多長的一個子串是與needle開頭部分一樣長的。next數(shù)組的長度正是needle的長度,并且有,next[0]=0,next[1]=0 (如果有next[1]的話)。next數(shù)組的計算是從左往右的,假設(shè)我們現(xiàn)在已經(jīng)完成了next[i]的計算,得到了next=[k],形成了下圖這種狀況。


圖中,整個長條表示的是needle串。已經(jīng)計算完了next[i]的意思就是說,我們已經(jīng)知道了如果在i位置失配,應該把下標i重置為幾;也就是說,已經(jīng)知道了在i之前一小串,有一串跟needle的開頭部分是一樣的,就是圖中的兩塊綠色,它們的長度正是k=next[i]。這兩塊綠色,前一塊是needle[0...k-1]這個子串,后一塊是needle[i-k...i-1]的子串。注意,我們在計算next[i]的時候,利用的是needle[i-1]這個字符,而并未訪問needle[i]。
接下來將要計算的是next[i+1],我們需要考慮的正是字符needle[i]。我們試著擴展上一次得到的部分匹配串,考慮needle[i]和needle[k],如下圖所示,考慮兩個藍色塊:

如果needle[i]==needle[k],則上一次得到的部分匹配串可以擴展一個位置,得到這樣的情形:

于是我們知道,next[i+1]=k+1,這是比較好理解的。那么如果needle[i]不等于needle[k],咋整?

Calm the hell down, and carry on...
既然next是從左往右計算的,那么needle從0到k這些位置,也是經(jīng)過同樣的計算方式得到的,我們來仔細看下這一段:

next[k]的值是已經(jīng)知道的,它表示如果在位置k失配,應該回到哪,同時也表示,在k之前有一個長度為next[k]的子串與needle開頭部分的一個長度為next[k]的子串是一樣長的。現(xiàn)在needle[i]!=needle[k],我們只能尋求更短的部分匹配串了。現(xiàn)在可以確定的是,圖中四個綠色的塊是相等的串。那如果不能從上面的長綠色塊擴展,能不能退而求其次,將本圖中的短綠色塊擴展呢?所以現(xiàn)在令k=next[k],然后查看新的needle[k]這個字符,如果它等于needle[i],說明這個擴展是可行的,因此next[i+1]=k+1。如果仍然沒法匹配,則重復上述過程,繼續(xù)令k=next[k]...直到找到這樣的匹配,或者直到k為0依然沒法找到這樣的匹配。
接下來用Python實現(xiàn)一下這個求next數(shù)組的過程:

def getNxt(s):
    nxt=[0]*(len(s))
    for i in range(1, len(s)-1):
        k=nxt[i]
        while k and s[k]!=s[i]:
            k=nxt[k]
        nxt[i+1]=k+1 if s[k]==s[i] else 0
    return nxt

注意如果我們沒法擴展部分匹配串,則置next[i+1]為0。也就是說,搜索過程中如果在needle[i+1]失配,則從needle開端位置重新開始匹配。

一旦得到了next數(shù)組,字符串的搜索過程就變得很簡單了。這個過程的思想是類似于next的求解過程的,本文前面也描述過。直接上代碼吧。

def strStr(self, haystack, needle):
        if not needle:
            return 0
        nxt=getNxt(needle)
        j=0
        for i in range(len(haystack)):
            while j and needle[j]!=haystack[i]:
                j=nxt[j]
            if needle[j]==haystack[i]:
                j+=1
            if j==len(needle):
                return i-j+1
        return -1

這里的j是needle的下標,當?shù)玫揭粋€匹配時,它加一,如果能加到len(needle),則查找成功了,返回當時的haystack起始下標。如果當前的hays[i]不能與needle[j]匹配,也就是在needle[j]發(fā)生了失配,則把j置為next[j],直到找到匹配,或者回到needle開始位置。

簡單分析一下算法的復雜度。KMP是一個線性時間復雜度的算法。對于查找函數(shù)strStr,主體部分有兩層循環(huán),咋會是線性的呢?首先,haystack的下標i是在每次for循環(huán)之后都增加1的;其次,while循環(huán)對needle做回退,如果站在haystack的視角來看,頂多能回退到已經(jīng)匹配了的子串那么長,也就是說對haystack的訪問頂多就一來一回兩次,所以均攤下來是2*len(haystack)的。
也可以把strStr的主體部分改寫一下,就更清楚了:

圖來自維基百科

上圖中,while循環(huán)的跳出條件是m+j>=len(haystack),這里的m是haystack的當前子串的起點,也就說,每次考察的是needle[j]是否匹配haystack[m+j]。如果前一個分支被執(zhí)行,則i會加1,以至于m+i會加1;如果后一個分支被執(zhí)行,則要么m加1,要么m+i-next[i],而i>next[i],因此m總會增加。因此不管哪個分支被執(zhí)行,m+i總會變大,因此時間復雜度是O(n)。

本文遵守知識共享協(xié)議:署名-非商業(yè)性使用-相同方式共享 (BY-NC-SA)簡書協(xié)議轉(zhuǎn)載請注明:作者曾會玩

Reference

[1] Knuth, Donald; Morris, James H.; Pratt, Vaughan (1977). "Fast pattern matching in strings". SIAM Journal on Computing 6 (2): 323–350.
[2] Knuth–Morris–Pratt algorithm, Wikipedia
[3] 插圖的一部分靈感激發(fā)自某次無意中翻到的博客,找不到鏈接了,請認領(lǐng)。

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

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