【字符串匹配】KMP算法之道


修訂於2012-06-18,心急的讀者可以着重看“有趣的字符串匹配提示”,這個例子看懂了,KMP也就差不多了。

閑話

上午算法考試的時候,感覺OK,前一兩星期幸好把圖算法都吃透了一遍,復習的時候節省了時間:)。前一半考題不理解背書的都可以,有幾題沒記過,不靠譜地照着理解寫下來。最后的吹水題讓我想起了之前的比賽,有一題是曹老師給的實驗題,剛好比賽上出現了,而且相似度極高。要是高考,曹老師可就紅了:)。這也讓我撿了便宜。

我們校區2012的招生計划出來了,結果我們校區悲催到只招30個法語本科生,也就是說2012的本科孩子只有30人。不知道法語的怎么看,但對這個校區的未來,我是看不到什么希望。“坑爹啊...”

有趣的字符串匹配“提示”

對於T=abaabab,P=abab,從T的第一個字符開始匹配:

  a b a a b a b
  a b a b      
第一次匹配 1 2 3 0      

可以看到,第四個字符已經匹配失敗了。此時如果采用最朴素的算法,也就是重新從第二字符開始匹配(不畫表了)。

KMP是這樣做的:既然上面第四個字符已經匹配失敗了,那么可以試着從已經匹配成功的前三個字符(即上面的“aba”)找到既是“aba”的后綴又是“aba”的前綴的字串,要求是此字串長度應該是所有滿足條件中最大的,暫且記為π(“aba”)。很顯然,π(“aba”)=1,因為

a b a
      a b a

因此猜測從第三個字符開始匹配可能會成功(其實應該是從第四個字符開始匹配,因為π(“aba”)=1已經暗示第三個字符“a”匹配成功)(猜測,只是猜測而已)

  a b a a b a b
第一次匹配 a b a b      
第二次匹配     a b
a b  

好吧,結果是不成功,因為出現了T中的第四個字符匹配失敗的情況。不過可以發現,KMP算法沒有像朴素算法那樣,從T的第二個字符開始匹配,轉而從T的第三個字符開始匹配,那為什么不從第二個字符開始匹配呢,因為從T的第三個字符開始匹配才有可能是成功的。如果你認為(或者說你有足夠的證據證明)從第二字符開始匹配會成功,那么上面找“既前綴又后綴”的結果:

b a
   a b a

即π(“aba”)=2應該是成立的,很明顯不是。

好吧,這里不成功, 前面成功匹配的字符沒有, 因此 π(null)=0.這逼着從T的第四個字符開始匹配,也是不成功:

  a b a a b a b
第一次匹配     a b      
第二次匹配       a
b a b

於是匹配成功。你將看到KMP也是這么做的,關鍵是如何計算上面的說的“既前綴又后綴”的結果——其實就是幫助匹配的輔助表。

KMP算法之道

問題定義:

字符串匹配問題:T=“www.daoluan.net/blog”,P=“daoluan”,問P是否在T中出現?答:是。

之前遇到的字符串匹配算法效率不是很看好,有限自動機之於最為朴素的窮舉法有一定的提高,但是初始化過程仍不樂觀,總體效率不高。奇葩的是,KMP算法初始化和匹配過程分別可以達到O(n)和O(m),實在是神奇。本篇文章目的就是吃透KMP。縱觀KMP,它無非就基於三個核心的結論,吃透這個三個結論,將KMP踩在腳下。

KMP和有限自動機字符串匹配一樣,借助了一個輔助一維表,但KMP的輔助表計算時間在O(m)內。這個輔助表是關於匹配內容P的前綴表。

在提及這些結論之前,先允許我啰嗦一下:

對於字串P,(k)P表示長度k的P的前綴;P(k)表示長度為k的P的后綴。比如P=abcdef,(3)P=abc,P(3)=def。

π(q)表示P的前綴(q)P的最長后綴P(k)的長度(也就是k要取最大值)。比如:P=ababababca,π(8)即(8)P=abababab的最長后綴P(k)的長度,k最大為6,因為

abababab|ca
ababab|abca

∴π(8)=6。

比如:匹配內容P=“daodaodaodaoluan”,那么關於P的前綴表 π(i) 即為:

P d a o d a o d a o d a o l  u a n  
π 0 0 0 1 2 3 4  5 6 7 8 9 0 0 0 0

π(q)有了定義,π*(q)可以有。通常加“*”表示所有,在這里也是。π*(q)是一個集合,它的所有成員可以迭代求出:

π*(q)={k|(k)P是(q)P的后綴且k<q}

={π1(q),π2(q),π3(q),π4(q)....},其中πn(q)=π[πn-1(q)]。

比如:同樣對P=ababababca,求π*(8), 有下圖結果(來自算法導論):

image.png

所以π*(8)={0,2,4,6}。對於其他的 π*(q)值也是這樣計算。這里的涉及了很多的定義,務必看懂,下面的三個結論才看得下去。如果太急,直接忽視這里,看上面的“有趣的字符串匹配”。

假設已經得到了關於P的輔助表,

kmp()
    m = strlen(P)
    n = strlen(T)
    π[m]
    kmptab(π)    //預處理輔助表
    q = -1
    for i=[0,n)
        while q>0 && P[q+1]!=T[i]
            q = π[q]
        if P[q+1]==T[i]
            q = q+1
        if q = m
            //    找到啦
        q = π[q]    //繼續剩余T的尋找

其中π[m]為輔助表。如果P[q+1]==T[i]能連續成立m次,那么可以找到T中的P。所以如果有輔助表的存在,匹配過程還是很容易理解的。

KMP 有三個結論,他們主要是用來計算輔助表的:

- π*(q)={k|(k)P是(q)P的后綴且k<q}。明顯.

- 如果π(q)>0,那么π(q)-1∈π*(q-1)。

∵π(q)=t,那么t<q,所以π(q)-1=t-1<q-1;

∵π(q)=t,∴(t)P是(q)P的后綴,(去掉(t)P和(q)P的最后一個字符)那么(t-1)P同樣也是(q-1)P的后綴;

根據結論一,得到 t-1∈π*(q-1),π(q)-1∈π*(q-1)總能成立。

- 圖示結論image.png

上面的圖中,如果匹配不成功, π 值會越來越小, 直到為0, 此時就需要重新從第一個字符開始匹配了.

上面的基礎就是為計算輔助表的。有了上面的結論:

kmptab(π)
    m = strlen(P)
    q = 0
    π[0] = 0
    for i=[1,m)
        while q>0 && P[q]!=P[i] // 與當前字符不匹配的時候, 才需要縮小 π
            q = π[q]    //    

        if P[q]=P[i]
            q = q + 1    //    與上圖中的做法一致
        π[i] = q

實在是太短了。

KMP的復雜度

KMP的復雜度一時我也說不清楚,借助了算法導論和Matrix67的手筆才略有領悟。KMP用到了平攤分析。就上面的kmptab(π)函數,從q的值來說,q = π[q]操作只會使的q越來越小,但總能q>0,因為q = π[q]是根據輔助表來得到的,而輔助表中最小為0。P[q]=P[i]條件成立,能使得q+1,也就是說q只有在這里才增加。最壞的情況,q被增加m-1次。所以while循環內的操作最多被執行m-1次的。平攤一下就是O(1)了。所以加上for循環,kmptab的復雜度為O(m)。

kmp主程序也是用這種平攤分析方法。

補充:KMP匹配過程中利用輔助表跳過了無效的檢查,直接將檢查過程跳轉到將來可能成功匹配的字符上。

本文完 2012-06-14

搗亂小子 www.daoluan.net/blog


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM