今天資料結構學習了兩個字串匹配演算法,bf演算法和kmp演算法。
課本講的過於籠統,不能理解kmp演算法,因此在網上找些資料後整理筆記如下。
字串匹配演算法就是在文字串中匹配模式串。
bf(brute force)演算法即暴力演算法。從第乙個字元開始匹配,每當匹配失敗時,模式串向右滑動一位。最後返回第一次找到的下標,若沒找到則返回-1。
ababc匹配失敗,模式串向右滑動一位。abc
a babc**實現如下:abc
intbf(
char t,
char p)
else}if
(p[j]
=='\0'
)return
(i - j)
;else
return-1
;}
假設文字串長度為m,模式串長度為n。
雖然bf演算法在最快情況下(第一次就匹配成功)的時間複雜度僅為o(m
)o(m)
o(m)
,但最差情況需要o(n
∗m)o(n*m)
o(n∗m)
。因此kmp演算法加以改進。
kmp演算法的改進之處在於每次回溯時都不止向右滑動1位,而允許一次滑動多位。
此時每次匹配失敗都不需要回溯到下乙個位置(即模式串不需要一位一位地向右滑動)。而是直接向右滑動k個字元,k是已經匹配的字元數。
因為模式串後續字元必不相等
文字串已經與模式串匹配了k個字元
所以文字串已經匹配的後續字元與模式串的首字元必不匹配。例如:
abcdeabcdef成功匹配了5個字元(abcde),模式串向右滑動5位abcdef
abcdeabcdef但是不能希望模式串永遠是由不重複的字元組成的,如果模式串中有重複字元怎麼辦呢?例如:abcdef
abcdabcdabde先當作沒有重複的字元處理,成功匹配了6個字元(abcdab),模式串向右滑動6位abcdabd
abcdabcdabde顯然這樣可能會錯過正確答案,因為本可以與模式串首字元匹配的字元(ab)被跳過,因此需要向左滑動以回溯。但不像bf那樣回溯到下乙個字元,而是回溯到下乙個模式串首字元出現的位置。如上面的字元需要回溯2位(字首ab和字尾ab重複,其長度為2),所以總體向右滑動6-2=4位abcdabd
abcd abcdabde這樣的滑動方式需要記錄模式串中每一段字元具有相同字首和字尾的長度用來回溯.abcdabd
模式串的各個子串
字首字尾
最大相同前字尾長度a無
無0aba
b0abca,ab
c,bc
0abcd
a,ab,abc
d,cd,bcd
0abcda
a,ab,abc…
a,da,cda…
1abcdab
a,ab,abc…
b,ab,dab…
2abcdabd……
0綜上所述,失配時,模式串向右滑動的位數為:已匹配字元數-已匹配字串的最大公共元素長度。
因為已匹配的字串一定是模式串的子串,所以回溯多少是只與模式串子串有關的量,也即只與失配時的字元在模式串中的序號有關。
用next陣列來儲存失配字元對應的應該回溯多少的值。
對於abcdabd,其next陣列為
模式串abc
dabd
next-10
0001
2可以發現next陣列就是把最大公共元素長度陣列整體向右移動一位,然後給next的第乙個元素賦值為-1。
用p表示模式串,next[j] == k的本質表示模式串的字元p[j]之前的子串中,有長度為k的最長相同前字尾。因此next陣列所包含的資訊就是在p[j]的位置失配時,下一步應該去哪?在p[j]失配時,根據先滑動再回溯的思想,即向右滑動j - next[j]個位數。之所以給next的第乙個元素賦值為-1,是因為如果第乙個元素就失配,仍需要向右滑動一位,此時j - next[j] = 0 - (-1) = 1。
此時向右滑動的位數變為:已匹配字元數-失配字元對應的next值。
使用遞迴的方法求next陣列。
首先假設已知next[0~j],且next[j] == k,如何求next[j + 1]?
例如:對於模式串abcdabce,j = 5。
模式串abc
dabd
enext-10
0001
?\已知next[5] == k(即1),這表示在p[5]失配時,之前已匹配的子串中有相同前字尾,其最大長度為k(即本例的p[0](a)和p[4](a))。因為k也表示了相同字首的長度,因此p[k]就是該相同字首的下乙個字元(本例的p[1](b))。並且p[j]表示該相同字尾的下乙個字元(p[5](b))。因此顯而易見:
此時考慮第二種情況,即p[j] != p[k]。例如以下模式串:
令α = ab(黃色表示),β = αcα(綠色表示),γ = βdβ(紅色表示)。則該字串為γeγcf。
已知 j = 23,next[23](即倒數第二位,c) == 11(γ的長度)。
此時p[j](e) != p[k](c),也即最大字首的下乙個字元和字尾的下乙個字元不同。因此就需要退而求其次尋找小一點的最大相同前字尾長度。而這個過程中由於字尾是不變的,因此以後綴為參考去尋找有無稍小一點的字首加上其下乙個字元等於當前的字尾。
對於本例來說,最大的字尾γc已經沒有相同字首了。那麼現在以βc為字尾去尋找下乙個稍小一點的字首,然而這個字首是βd。那麼再以αc去尋找,bingo!這時字首剛好也是αc,因此next[24] == 3。
而如果到最後也沒有找到(即k一直尋找到next[0],也即k == -1),那麼相同前字尾的長度就是0了。
總結下來就是:
**如下:
int
*getnext
(char p)
return next;
}
此時的next陣列還有乙個小問題。即不該出現p[j] == p[next[j]]
。理由是當p[j]與t[i]失配時,下次匹配必然是p[next[j]]與t[i]匹配。而如果p[j] == p[next[j]],那麼本次匹配則一定會失敗,因此這將會是一次冗餘的匹配。
考慮下例:
abacabab第一次失配後變為:abab
abacabab那麼本次匹配也一定會失配。abab
優化的方法就是在出現p[j + 1] == p[next[j] + 1]
的情況時,讓下一次的匹配從p[next[j]]與t[i]變為p[next[next[j]]]與t[i]。
即不是next[j + 1] = next[j] + 1
,而是next[j + 1] = next[next[j] + 1]
。
**如下:
while
(j <
strlen
(p)-1)
else
}
完整的kmp演算法**如下:
int
kmpsearch
(char
* t,
char
* p)
else}if
(j == plen)
return i - j;
else
return-1
;}
由於最壞的情況也只遍歷一次文字串,再加上確定next陣列得遍歷一次模式串,因此kmp演算法的時間複雜度是o(n
+m)o(n+m)
o(n+m)
。以上關於kmp思路的總結,參考了從頭到尾徹底理解kmp這篇文章。
關於如何確定next陣列的部分也參考了這篇文章。
BF和KMP演算法
字串匹配演算法 include include using namespace std define ok 1 define error 0 define overflow 2 typedef int status define maxstrlen 255 使用者可在255以內定義最長串長 type...
字元 BF 和 KMP演算法
kmp演算法是一種改進的字串匹配演算法,由d.e.knuth與v.r.pratt和j.h.morris同時發現,因此人們稱它為克努特 莫里斯 普拉特操作 簡稱kmp演算法 在介紹kmp演算法之前,先介紹一下bf演算法。一.bf演算法 bf演算法是普通的模式匹配演算法,bf演算法的思想就是將目標串s的...
BF演算法與KMP演算法
using system namespace kmp else count if j lenb 1 return i lenb else return 0 stathread static void main string args lenb p1.length,p2.length reval km...