第三十二章 字符串匹配

設T爲全局查詢域,P爲要查詢的字符串,查詢結果是:返回一個或多個距離T開始的偏移
後綴:字符串A是字符串D=X+A的後綴,記A -} D
前綴:字符串A是字符串D=A+x的前綴,機A {- D

樸素算法

這個算法就是每個人都能想到的最樸素的算法
算法:
naive_string_matcher(T,P)
    n=T.lenth
    m=P.lenth
    for s=0 to n-m
        if P[1...m]==T[s+1...s+m]
            printf s

代碼實現:


Rabin-Karp

我們主要是體會一下這個算法思想,因爲它假設字符出自集合{0,1,2...9}(10進制,所以設變量d=10)這樣就可以將要查詢的P看成一個m=P.length位的數,然後對一個數q求模。如果兩個m位字符串代表的數模q相等,則這兩個字符串纔有可能相等,進一步判斷如果相等輸出偏移量。

1、先求出h=dm-1(mod q)

2、求出P和T的前m個字符串代表數的模p=P[1,2,...m](mod q)、 t0=T[1,2,...m](mod q)的值

3、已知t(i)的值,我們能在常數時間求出t(i+1)的值,根據公式
   t(s+1)=(d*(t(s)-T[s+1]*h)+T[s+m+1])mod q     t(s)=T[s+1,s+2,....s+m](mod q)

算法:
ranin_karp_matcher(T,P,d,q)
    n=T.length
    m=P.length
    h=d(m-1)mod q
    p  for i=1 to m //除法求餘
        p=(d*p+P[i])mod q
        t0=(d*t0+T[i])mod q
    for s=0 to n-m
        if p==t(s)
            if P[1...m]==T[s+1...s+m]
                printf s
        if s<n-m
            t(s+1)=(d*(t(s)-T[s+1]*h)+T[s+m+1])mod q

代碼實現:


我們也可以將P的m的字符求和然後模q,p=(P[1]+P[2]+...P[m])mod q
然後求出t0=(T[1]+T[2]+...T[m])mod q,也可以在常數時間內求出t(s+1)
根據公式:t(s+1)=(t(s)-T[s]+T[s+m+1])mod q



有限自動機


狀態0:T[s]!=P[1]
狀態1:P[1]
狀態2:P[1]->P[2]
狀態3:P[1]->P[2]->P[3]
.
.
.
狀態m:P[1]->P[2]->P[3]...->P[m]

如果T中存在字符串P則T中一定存在一個這樣的狀態P[1]->P[2]...->P[m],只有m狀態纔是正確匹配到了P

當在狀態i的時候,設T[k]=P[i]如果T[k+1]!=P[i+1],則狀態不會轉移到i+1。那麼狀態會轉移到什麼呢?答案是任何<i的狀態都有可能,這是需要經過計算的。
存在字符串A是P[1,2,...i]的後綴&&是P的前綴,我們轉移到最長A的狀態中,因爲這是緊接下來的第一個可能成功匹配的偏移。(這一點將會在後面證明)

設集合G={T中出現過的字符},我們把狀態 i 經過字符c(each c from G)轉移到狀態 j 計算出來並保存到一個二維數組theta中即j=theta(i,c)

例如:G={a,b,c} P=ababaca 結果如圖:



計算theta的一種算法如下
算法:
compute_transition_function(P,G)
    m=P.length
    for q=0 to m
        for(each charater a from G)
            k=min(m+1,q+2)//仔細斟酌 k= 1 to m+1
            do
                k=k-1
            //P[1..k]是P[1..q]+a的後綴,由k=k-1可知這裏去的是最長的A
            while P[1,2,...k]-}P[1,2,...q]+a  //k=0時空字符串是任何字符串的後綴
            theta(q,a)=k



計算出theta後就可以在線性時間完成匹配

算法:

finite_automaton_matcher(T,theta,m)
    n=T.length
    q=0
    for i=1 to n//注意是[0,n]
        q=theta(q,T[i])
        if q==m
            print i-m

代碼實現:


Knuth-Morris-Pratt


定義:pai[i]的值爲最長P[1...i]的真後綴同時是P[1....m]的前綴的長度

我們先假設:"每當T[s+1...s+i]的下一個字符不能匹配時,將偏移s向前移i-pai[i]位(s=s+i-pai[i])繼續向前匹配,這樣能匹配到所有的結果"是正確的

現在來說明假設的正確性
T[s+1...s+i]能正確匹配,T[s+i+1]不能。此時我們將s直接向前移動i-pai[i]位,那麼接下來的pai[i]位與P[1...pai[i]]是匹配的,可以繼續向前匹配。只要說明之間跳過的點不能匹配到正確結果就可證明假設成立。

如果s到s+i-pai[i]之間有能夠正確匹配的,那麼存在一個s'使得T[s'+1...s+i]是P[1...m]的前綴,且值大於pai[i]。這與pai[i]最長矛盾。

求這樣一個pai的原因是:如果T[s+1...s+i]與P[1...i]匹配,下一個不能匹配即T[s+i+1]!=P[i+1]。站在偏移s處可以看到T[s+1...s+i]的值,因爲我們知道P[1...i]的值,他們兩者相等!所以應該預先求出每個P[1...i]的pai[i],以免每次求最長T[s+1....s+i]的真後綴同時是P[1...m]的前綴。根據pai[i]的值,於是我們設置偏移s=s+i-pai[i]。


如何計算pai

    現在pai[i]的值爲k,如果能匹配就pai[i+1]=k+1。如果不能匹配就遞歸的考慮與P[i+1]能否匹配,能匹配時pai[i+1]=k+1,不能匹配時k=pai[k]。直到k=0時結束,並令pai[i+1]=0。

當pai[i]=k=0時,就回到了最初的匹配,P[i+1]是否等於P[k+1]。

算法:

compute_prefix_function(P)
	m=P.length
	let pai[1...m] be a new array
	pai[1]=0
	k=0
	for q = 2 to m//find q <--> k
		while k>0 and P[k+1]!=P[q]
			k=pai[k]
		if P[k+1]==P[q]
			k=k+1
		pai[q]=k
	return pai

kmp算法:

KMP_matcher(T,P)
	n=T.length
	m=P.length
	pai=COMPUTE_PREFIX_FUNCTION(P)
	q=0						//number of characters matched
	for i = 1 to n 				//scan the text from left to right
		while q>0 and T[i+1]!=P[q]		
			q=pai[q]			//next character does not match
		if T[i+1]==P[q]	
			q=q+1
		if q==m 				//is all of P matched
			print "Patter occurs with shift" i-m
			q=pai[q]			//look for the next match

代碼實現:

未完待續...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章