修訂於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的第三個字符開始匹配才有可能是成功的。如果你認為(或者說你有足夠的證據證明)從第二字符開始匹配會成功,那么上面找“既前綴又后綴”的結果:
a 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), 有下圖結果(來自算法導論):
所以π*(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)總能成立。
上面的圖中,如果匹配不成功, π 值會越來越小, 直到為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