KMP算法,以為一個簡簡單單的算法,看了我一天時間竟然沒有看懂...果然圖樣圖撕破了,三位大師提出的算法豈是我等屌絲能夠迅速的理解的?不過話說看了這次算法才知自己的智力有多么的吃緊,還是要努力學習呀~智力不行就要加把勁了。(接下來字符串匹配算法均參考算法導論)
字符串匹配,算法的模型不用提出大家都知道,僅僅是在文本T字符串中精確匹配模式串P,簡簡單單,輕輕松松的就知道這么一個模型,自然而然的能夠想到一個最笨最實用的算法,朴素算法,朴素算法就是逐個比對,然后在文本串中下移一位在進行逐個比對,算法復雜度O((n-m+1)m),
不過這種簡單的方法的時間復雜度不是我們能夠容忍的,提高一點有一個Rabin-Karp的算法,此算法的最壞時間復雜度沒有提高,但是平均情況比較好,而且它這個思想可以借鑒,就是把匹配當做算術運算,后來取模比對,針對剩余的少部分精確比對~不過這不是重點,
接下來的就是重量級的算法,KMP算法,它在預處理時間和匹配時間均達到了線性時間~O(m)的預處理時間,O(n)的匹配時間,非常完美的理論,不是么?網上很多資料都是關於next函數的,這里我參考算法導論里面的KMP算法,其思想雖然一致,但是不同於next函數的原理,其原理如下:(匹配過程)
i 1 2 3 4 5 6
a b a b a b a a b a b
a b a b a c b
a b a b a c b
j 1 2 3 4 5 6 7
j 1 2 3 4 5 6
這里比對過程是i和j的變化,如果i和j對應的地方相同,則同時+1,繼續走下去,當出現T[i]和P[j]出現不同時,如上圖中的P[6]!=T[6],此時KMP考慮的是i不進行回溯,而是考慮挪動j,使得j變化為一個更小的值,小於j,這樣的變化效果是P整體向后移,但是此時不能夠導致T[6]前面的i的匹配性質,及挪動后T[6]前面的元素和P對齊之后還是要相同的,所以這里j=5改為j=3,挪動后效果如紅色標注的P所示,這是T[6]=P[4],兩個串的表示i和j可以挪動下去;
i 1 2 3 4 5 6 7 8 9 10 11
a b a b a b a a b a b
a b a b a c b
a b a b a c b
3
a b a b a c b
1
a b a b a c b
0
j 1 2 3 4 5 6 7
但是接下來挪動的時候又產生了問題,T[8]!=P[6],這時候如同上面的方式,挪動j=5的位置,首先挪動至最近的保持T[8]前保持性質的部分,其實這里保持性質及P[5]的后綴的最長前綴...說到概念可能就不知道了,還是理解保持性質吧,挪動至紅色后發現T[8]!=P[4],所以這里還要挪動j=3的位置,挪動成j=1,還是不能滿足,所以還需要挪動,這時j=0了,及這時候T[8]和P[1]比較了,這里剛好一樣,所以i可以繼續向后挪動了;
以上的移動均是參考了一個數字,該數字是保證某個位置的時候P和T能夠保持性質,但是這個性質僅僅和P有關,及上面說的P的后綴的最長前綴,因為以上匹配過程用到了這個數據,所以接下來就是這個數據的計算的算法了,其實這個算法才是讓我糾結的地方,糾結了好久,這里發現一個可以很容易理解的地方,分享給大家;
上面算法的線性時間需要用到均攤分析的知識~可以仔細思考一下,線性的時間,
這里圖解一下計算后綴的最長前綴吧,這里這個記為PI[j],這里PI[j]可以根據PI[1],PI[2]...PI[j-1]計算得出,
如圖所示,PI[j-1]為標記的凸包的長度,這里如果P[lengh[PI[j-1]]+1]=P[j],這里PI[j]及為PI[j-1]長度加1,如果P[lengh[PI[j-1]]+1]!=P[j],則這個需要在更加前面找,這里及遞歸的找,畫圖如下,
及黑色線標注的位置是否與P[j]相同選擇是否繼續遞歸,黑色標注的位置的選擇為PI[ PI[j-1] ] + 1,PI[j-1]的位置為紅色的那段,因為不可能為紅色,所以要縮小一下,這里示意為綠色的圈,綠色的圈挪動到前邊的紅色部位,所以綠色的圈在紅色的部位找到前綴為黑色的部分,所以PI[ PI[j-1] ]是這樣得出的,這樣遞歸下去就能夠得出PI[j]的值了,這樣就可以根據計算得出的PI[j]值運用匹配算法了,
不知道上面的解釋是否能夠解釋的清楚,一個遞歸的過程,分析算法復雜度也是一個均攤分析的方法,同樣是線性的,
模式串的預處理的偽代碼如下:
COMPUTE-PREFIX-FUNCTION(P) m=length(P) PI[1]=0 //初始化 k=0 //初始化 for q=2 to m while k>0 and P[k+1]!=P[q] //結束條件至0或者相等 k=PI[k] //遞歸的過程 if P[k+1]=P[q] // 增加的過程 k=k+1 PI[q]=k //賦值為k,下一個計算已k為起點向下遞歸 return PI
KMP匹配過程執行的偽代碼如下:
KMP-MATCH(T,P) n = length[T] m = length[P] PI=COMPUTE-PREFIX-FUNCTION(P) j=0 for i=1 to n while j>0 and P[j+1]!=T[i] //如果不等,挪動P,根據計算好的PI j=PI[j] if P[j+1]=T[i] //如果相等,則向后繼續匹配 j=j+1 if j=m //匹配m個后則成功 printf(匹配成功) j = PI[j]
以上是偽代碼描述,上面說描述的參考算法導論的KMP思想,網上還有一種next方式的,有空研讀一下比較差別;
除了上述算法還有一些優秀的字符串匹配算法,基於自動機的,BM算法等等,
如果想要了解一下有限自動機方法可以繼續看下去,這種方法可KMP算法可以看做等價,
有限自動機字符串匹配算法簡單介紹如下:
給定模式P[1...m],其對應的字符串匹配自動機定義如下:
1、狀態集Q為{0,1,...,m},初始狀態q0為0狀態,狀態m是唯一的接受狀態。
2、對於任意的狀態q和字符a,變遷函數δ由如下等式定義:δ(q,a)=σ(Pq a)
這里的解釋一下σ(x)的定義,其為相應P的后綴函數。σ(x)為x的后綴 P的最長前綴的長度:σ(x)=max{k:Pk是x的后綴}
簡單的舉例如下:
i 1 2 3 4 5 6 7 8 9 10 11
T[i] a b a b a b a c a b a
狀態 1 2 3 4 5 4 5 6 7 2 3
上述是匹配的過程,下面是轉移表:
a b c P
0 1 0 0 a
1 1 2 0 b
2 3 0 0 a
3 1 4 0 b
4 5 0 0 a
5 1 4 6 c
6 7 0 0 a
7 1 2 0
自動機的過程與KMP可以轉換,只不過自動機考慮了T[i]不匹配過程中的具體字母表的轉移,然后KMP只是不匹配的情況下先進行挪動,挪動后發現不匹配之后再進行挪動?直至挪動為0的時候,抑或是一直不匹配i++,此時忽略j;自動機只是具體考慮了此時如果T[i]如果不等於P[j],並且此時T[i]的具體取值已經指定,在此值指定的情況下應該怎樣的轉移~
a b a b a c a
0 0 1 2 3 0 1
上面是KMP計算出的模式函數,首先a值是T[i]和P[1]如果相等,則j++,向后移動,但是如果不匹配,此時如同自動機轉移表第一行,為b or c,此時就應該i,j同時++了,此時KMP如果發現T[i]和P[1]不相等,則上面算法i++,j取值仍為0,等價於i和j同時++了;
這時考慮b,如果a匹配了,P[2]和T[i]匹配,此時j++=2了,這時候移動兩位了,自動機表現匹配兩個字母,所以為狀態2,但是如果不相等呢?此時考慮了b,必定T[i]和P[1]已經匹配了,KMP中不匹配則考慮移動P,根據值j重新改為0,及向右移動兩位,然后自動機此時讀入不等的可能為a或者c,如果為a,則KMP移動兩位之后匹配上了一位,j++=1,如果為c,移動之后仍然不能匹配,所以i和j同時仍然移動,KMP表現i++,j重新賦值為0;
...
這次考慮一個中間的值,比如c的位置,如果知道T[i]值為多少,則可以知道具體的移動多少,然而KMP中是根據P計算的模式值,所以這里不知道T[i]的值,它有可能是最壞的值,所以這里取得的是最壞的值;所以這里計算自動機轉移函數可以考慮KMP中已經計算好的結果,這樣提升時間復雜度,
...
所以自動機匹配和KMP算法有一定的等價性,上面的描述比較凌亂,希望能夠對你有所啟發,