BF、KMP、BM、Sunday算法講解
字串的定位操作通常稱作串的模式匹配,是各種串處理系統中最重要的操作之一。
事實上也就是從一個母串中查找一模板串,判定是否存在。
現給出四種匹配算法包括BF(即二維循環匹配算法)、KMP、BM、Sunday算法,着重講KMP算法,其他算法盡量詳細講解,有興趣的讀者可自行查找其它相關資料了解其它算法,當然本文也會推薦一些網址供讀者參考。
事實上本博文也是作者閱讀了其它博文,然后根據自己的在理解過程中遇到的問題加以闡述,總結而來的,尤其是多次閱讀了July的博文。
本文可能會給一部分讀者閱讀帶來不便,所以在本文開始部分就推薦讀者相關博文,若讀者已經掌握了相關算法,就不要浪費時間繼續瀏覽本博文了,干點其他有意義的事吧:
July博文(強力推薦)http://blog.csdn.net/v_july_v/article/details/7041827
注:為方面書寫,S稱作母串,T稱作模板串
一、BF:二維循環匹配算法
算法比較簡單,不再給予相關解釋,直接給出代碼,如下:
1 int Search(const string& S, const string& T) { 2 3 int i = 0; 4 int j = 0; 5 6 while(S[i] != ‘\0’ && T[j] != ‘\0’) { 7 if(S[i] == T[j]) { 8 ++ i; 9 ++ j; 10 } else { 11 i = i - j + 1; 12 j = 0; 13 } 14 } 15 16 if(T[j] == ‘\0’) 17 return i - j; 18 19 return -1; 20 }
從代碼可以看出,此算法的時間復雜度為O(),相當耗時,一般不采用此算法。但是讀者一
定要明確知道此算法的運行過程,KMP算法就是在此基礎上改進而來的。
二、KMP算法
此算法是D.E.Knuth與V.R.Pratt 和J.H.Morris同時發現的,取三人名字首字母便得到了KMP,
也稱作為克努特-莫里斯-普拉特操作。此算法的時間復雜度為O(n+m),n為S的長度,m為T的長
度。
假設S為
BBCABCDABABCDABCDABDE
T為
ABCDABD
如果按照二維匹配算法,那么過程如下:
第一步,從S與T首字符開始匹配
B B C A B C D A B A B C D A B C D A B D E
↨
A B C D A B D
第二步,匹配到此處(為方便說明,省去了不必要的匹配過程)
B B C A B C D A B A B C D A B C D A B D E
↨
A B C D A B D
第三步,根據首字符相匹配可得,可以繼續執行T遍歷,查找S中的子串
B B C A B C D A B A B C D A B C D A B D E
↨
A B C D A B D
到了此處我們發現出現了不匹配現象,如果按照二維循環匹配算法,那么下一步必然會這樣:
(1)
B B C A B C D A B A B C D A B C D A B D E
↨
A B C D A B D
緊接着又會出現不匹配,直到運行到下面的結果:
(2)
B B C A B C D A B A B C D A B C D A B D E
↨
A B C D A B D
讓我們算一下,從第三步下標的位置到(2)這一步T的下標共移動了幾次呢??答案是6次
(但願我沒算錯)!!如果我們直接從第三步移動到(2)這一步是不是只需要一次就可以了呢
,這樣算下去的話,能減少多少不必要的匹配時間!!
因此,KMP算法誕生了,按照以上的步驟(運行到第三步后直接調轉到(2)),給出相關代碼:
1 int KMP_Search(const string& S, const string& T) { 2 3 int i = 0; 4 int j = -1; 5 int sLen = S.size(); 6 int tLen = T.size(); 7 8 while(i < sLen && j < tLen) { 9 if(j == -1 || S[i] == T[j]) { 10 ++ i; 11 ++ j; 12 } else { 13 j = next[j]; 14 } 15 } 16 17 if(j == tLen) 18 return i - j; 19 20 return -1; 21 }
以上代碼可以看出,關鍵的實現在於next數組,所以讀者會問next數組里面保存了什么,能
夠實現這么強大的功能??下面將給予解答……
事實上next[k]數組保存了T前k-1位相等的后綴和前綴的最大長度,next[0]為-1。
以T為”ABCDABD”為例,即:
k值 |
前綴 |
后綴 |
最長相等的前綴與后綴 |
next[k] |
0 |
空 |
空 |
空 |
-1 |
1 |
空 |
空 |
空 |
0 |
2 |
A |
B |
空 |
0 |
3 |
A、AB |
C、BC |
空 |
0 |
4 |
A、AB、ABC |
D、CD、BCD |
空 |
0 |
5 |
A、AB、ABC、ABCD |
A、DA、CDA、BCDA |
A |
1 |
6 |
A、AB、ABC、ABCD、ABCDA |
B、AB、DAB、CDAB、BCDAB |
AB |
2 |
7 |
A、AB、ABC、ABCD、ABCDA、ABCDAB |
D、BD、ABD、DABD、CDABD、BCDABD |
空 |
0 |
接着,讀者可能會問為什么要保存它??
不得不說,上述表格中的數據確實說明不了什么,而且還有可能使讀者更加困惑,不過沒關
系,請耐心繼續往下看,一會就會明白了……
借用第三步得到的狀態:
B B C A B C D A B A B C D A B C D A B D E
↨
A B C D A B D
S與T分別匹配到A與D處,如果按照二維匹配算法,下一步就會這樣:
B B C A B C D A B A B C D A B C D A B D E
↨
A B C D A B D
S的’B’與T的’A’匹配辨別,事實上就是(加重部分)的匹配,
B B C A B C D A B A B C D A B C D A B D E
↨
A B C D A B D
不成功匹配字符串”BCDABA”中的”BCDAB”正好是T中匹配成功的部分,也就是T中
”ABCDAB”的字串,也是”ABCDAB”的一個后綴,這個后綴正在與”ABCDAB”進行匹配,
那么所可能匹配成功的最大長度則是”ABCDAB”刪去最后一個字符得到字符串”ABCDA”
的長度,這個串也是”ABCDAB”的一個前綴,這也就是相當於”ABCDAB”的前綴和后綴在
匹配,講到這里,讀者應該似乎明白了點什么吧,上述表格似乎有點說法,是吧??那么,
接着說,既然是一個字符串的前綴和后綴進行匹配,也就是”BCDAB”和”ABCDA”進行匹配,
即:
B C D A B B C D A B B C D A B
↨ ↨ ↨
A B C D A A B C D A A B C D A
B C D A B B C D A B
↨ ↨
A B C D A A B C D A
其實以上五個匹配過程也可以這樣看:
B C D A B C D A B D A B
↨ ↨ ↨
A B C D A A B C D A B C
A B B
↨ ↨
A B A
你發現了什么??
好吧,那我說一下我的發現:前綴”ABCDA”一直在與后綴”BCDAB”匹配,如果匹配不成功
,那么前綴”ABCDA”的前綴就再與后綴”BCDAB”的后綴繼續匹配……所能匹配成功的最大長度
就是這一步:
B C D A B
↨
A B C D A
這一步所得到的最大長度就是1,回到next數組,此時的k值恰好為5,next[5]為1,繼續匹
配將會得到:
B C D A B
↨
A B C D A
此時,k為6,next[k]為2!!
講到這里,不知道讀者是否明白??我已經盡力讓讀者明白了,如果仍不明白,看來我的表
述能力存在缺陷,有待提高……
總結起來,就是上述表格所描述的那樣,T的前k-1位的后綴和前綴一直在匹配。
希望讀者能夠好好理解一下上述過程,如果實在不理解,可以留言抑或參考推薦的博文。
OK,下一步用代碼求解next數組。
1 void getNext(const string& T) { 2 3 next[0] = -1; 4 int i = 0; 5 int j = -1; 6 7 while(T[i] != ‘\0’) { 8 if(j == -1 || T[i] == T[j]) { // 這個if應該難不倒讀者 9 ++ i; 10 ++ j; 11 next[i] = j; 12 } else { // 關鍵在這里,如果不相等,那么j就 13 j = next[j]; // 必須回退 14 } 15 } 16 17 }
到了這里,至於代碼為什么要這么寫需要讀者自己獨立思考一下了……
至於時間復雜度為什么是O(n+m)就不給予證明了,推薦的July博文有明確證明。
注:n為S長度,m為T長度
下面再給出next數組的優化。
讀者可能會問next數組為什么需要優化??在這里舉出一個例子進行說明:
假設母串S為
aaabaaaab
模板串T為
aaaab
當匹配到這里的時候:
a a a b a a a a b
↨
a a a a b
按照上述next數組保存的結果進行匹配,可以發現S[3] != T[3],那么下一步就需要根據
next[3] = 2進行匹配,然后再根據next[2] = 1進行匹配……直到匹配到T的首字符仍然匹配不
成功為止。那么在求解next數組的時候就可以預判一下,使得在上述匹配不成功的時候直接
滑向首字符,省去不必要的匹配過程。也就是說在S[i] == T[j] 時,當S[i + 1] == T[j + 1] 時,
不需要再和T[j]相比較,而是與T[next[j]]進行比較。那么,next數組求解可以改為下述代碼:
1 void getNextval(const string& T) { 2 3 next[0] = -1; 4 int i = 0; 5 int j = -1; 6 7 while(T[i] != ‘\0’) { 8 if(j == -1 || T[i] == T[j]) { 9 ++ i; 10 ++ j; 11 // 修改部分如下 12 if(T[i] != T[j]) 13 next[i] = j; 14 else next[i] = next[j]; 15 } else { 16 j = next[j]; 17 } 18 } 19 20 }
ok,整理一下KMP完整代碼:
1 void getNext(const string& T) { 2 next[0] = -1; 3 int i = 0; 4 int j = -1; 5 6 while(T[i] != ‘\0’) { 7 if(j == -1 || T[i] == T[j]) { 8 ++ i; 9 ++ j; 10 next[i] = j; 11 } else { 12 j = next[j]; 13 } 14 } 15 } 16 17 void getNextval(const string& T) { 18 next[0] = -1; 19 int i = 0; 20 int j = -1; 21 22 while(T[i] != ‘\0’) { 23 if(j == -1 || T[i] == T[j]) { 24 ++ i; 25 ++ j; 26 27 if(T[i] != T[j]) 28 next[i] = j; 29 else next[i] = next[j]; 30 } else { 31 j = next[j]; 32 } 33 } 34 } 35 36 int KMP_Search(const string& S, const string& T) { 37 int i = 0; 38 int j = -1; 39 int sLen = S.size(); 40 int tLen = T.size(); 41 42 while(i < sLen && j < tLen) { 43 if(j == -1 || S[i] == T[j]) { 44 ++ i; 45 ++ j; 46 } else { 47 j = next[j]; 48 } 49 } 50 51 if(j == tLen) 52 return i - j; 53 54 return -1; 55 }
ok, kmp算法介紹完畢,下面介紹BM算法~
三、BM算法
事實上KMP算法並不是最快的匹配算法,BM算法(1977年,Robert S.Boyer和J
Strother Moore提出)要比KMP算法快,它的時間復雜度為O(n)(平均性能為O(n),
但有時也會達到O(n * m),而且書寫代碼要復雜,不細心的讀者很容易寫錯),BM算法
采用從右向左比較的方法,同時應用到了兩種啟發式規則,即壞字符規則 和好后綴規則 ,
來決定向右跳躍的距離。
例如:
第一步:
A B C D E F G H I
↨
C D E F G F
然后向前匹配:
A B C D E F G H I
↨
C D E F G F
這就是從后往前匹配。
BM算法定義了兩種規則:
注:為方面書寫,S稱作母串,T稱作模板串
注:規則讀一遍即可,下述圖文解釋匹配過程可完全解釋規則含義。
注:網頁上關於BM算法的規則說明原理都是一樣的,只不過是表述不同。
A B C D E F G H I
↨
C D E F E F
如上圖所示,藍色字符串“EF”就是好后綴,黃色字符“D”就是壞字符。
1)壞字符規則
在BM算法從右向左掃描的過程中,若發現某個字符x不匹配,則按如下兩種情況討論:
I.如果字符x在T中沒有出現,那么從字符x開始的m個文本顯然不可能與S在此處的字符
匹配成功,直接全部跳過該區域即可。
II.如果x在T中出現,選擇最右該字符進行對齊。
2)好后綴規則
若發現某個字符不匹配的同時,已有部分字符匹配成功,則按如下兩種情況討論:
I.如果在T中其他位置存在與好后綴相同的子串,選擇最邊右的子串,將S左移使該子
串與好后綴對齊(相當於T右移)。
II.如果在T中任何位置不存在與好后綴相同的字串,查找是T中否存在某一前綴與好后
綴相匹配,如果有選擇最長前綴與S對齊,相當於S左移或者T右移;如果不存在,那么直
接跳過該后綴,T的首字符與S好后綴的下一字符對齊。
壞字符規則圖文解釋:
(1)
A B C D E F G H
↨
H I J K
不匹配,直接跳過,得到:
A B C D E F G H
↨
H I J K
(2)
A B C D E F G
↨
A D D F
字符"D"在T中存在,那么得到:
A B C D E F G
↨
A D D F
好后綴規則圖文解釋:
(1)
A B C D E F G H I J K
↨
A E F A E F
字符"A"與"D"不匹配,好后綴"EF"在T中存在,那么得到:
A B C D E F G H I J K
↨
A E F A E F
(2)
A B C D E F G H I J K L
↨
F G F A E F G
T中存在前綴“FG”與后綴“FG”相匹配,那么得到:
A B C D E F G H I J K L
↨
F G F A E F G
如果是這樣:
A B C D E F G H I J K L M N
↨
F A F A E F G
不存在前綴與任一后綴匹配,那么得到:
A B C D E F G H I J K L M N
↨
F A F A E F G
ok,原理說明完畢,下面就是代碼求解:
注:網上諸多作者均給出了詳細求解代碼,可是求解代碼比較晦澀難懂,沒有比較好的注釋
以幫助讀者完全理解,所以在此根據編者自己的理解,給出了晦澀代碼段的相關注釋。
先給出BM算法的匹配代碼:
注:bmG表示好后綴數組,bmB表示壞字符數組
1 int BM(const string& S, const string& T, int bmG[], int bmB[]) { 2 3 int sLen = S.size() - T.size(); 4 int tLen = T.size(); 5 int i = 0; 6 7 get_bmB(T, bmB); 8 get_bmG(T, bmG); 9 10 while(i <= sLen) { 11 int j = tLen - 1; 12 // 出現不匹配字符或者匹配成功循環結束 13 for( ; j > -1 && S[i + j] == T[j]; --j) ; 14 15 // 匹配成功 16 if(j == -1) 17 return i; 18 19 // 選擇好后綴與壞字符中移位最大者,tLen - 1 - j表示的是該字符距字符串尾部的距離,bmB[S[i + j]]表示的是該字符 20 // 出現在T中的最右位置距字符串尾部的距離。 21 i += max(bmG[j], bmB[S[i + j]] - (tLen - 1 - j)); 22 } 23 24 return -1; 25 }
那么,下面就需要求解bmG數組和bmB數組了,由於求解bmG數組比較麻煩,所以先給出
bmB數組的求解代碼:
1 void get_bmB(const string& T, int bmB[]) { 2 3 int tLen = T.size(); 4 5 // MAXSIZE 表示字符種類數目 6 // 壞字符不存在T中時,直接后移此片段 7 for(int i = 0; i < MAXSIZE; ++ i) { 8 bmB[i] = tlen; 9 } 10 11 // 壞字符存在T中時,選擇T中最右字符存在的位置 12 for(int i = 0; i < tLen; ++i) { 13 bmB[T[i]] = tLen - 1 - i; 14 } 15 16 }
代碼很簡單,讀者稍加理解應該沒問題。
OK,下面給出bmG數組的求解代碼:
1 // bmGLength[i]代表的是T在以該字符為后綴的字符串與T的后綴所能匹配的最大長度 2 int bmGlength[N]; 3 4 void get_bmG(const string& T, int bmG[]) { 5 6 // 求解好后綴數組那么必先得到匹配不成功處好后綴的長度是多少 7 get_bmGLength(T, bmGLength); 8 9 // 那么按照好后綴的原理,先默認選擇匹配不成功之處不存在好后綴的情況 10 int tLen = T.size(); 11 for(int i = 0; i < tLen; ++ i) { 12 bmG[i] = tLen; 13 } 14 15 16 // 注:以下代碼中i之所以 < tLen - 1是因為下標tLen - 1處不存在好后綴,只有壞字符 17 18 // 然后是選擇匹配不成功之處T中存在包含該字符的前綴與好后綴相匹配的情況 19 int j = 0; 20 for(int i = tLen - 2; i >= 0; -- i) { 21 // 若存在前綴與后綴相匹配,那么此處所保存的數值必然是i + 1 22 if(bmGLength[i] == i + 1) { 23 for(; j < tLen - 1 - i; ++ j) { 24 // 還沒有變化過時方可賦值 25 if(bmG[j] == tLen) 26 bmG[j] = tLen - 1 - i; 27 } 28 } 29 } 30 31 // 最后就是不匹配處T中存在與好后綴相匹配的字符串,選擇最右 32 // 字符串 33 for(int i = 0; i < tLen - 1; ++ i) { 34 bmG[tLen - 1 - bmGLenth[i]] = tLen - 1 - i; 35 } 36 37 }
那么接着給出bmGLength數組的求解代碼:
注:代碼中有很多地方尤其是數組下標中加了相關括號,這是方便讀者理解的地方,希望
讀者要稍加注意
1 void get_bmGLength(const string& T, int bmGLength[]) { 2 int tLen = T.size(); 3 bmGLength[tLen - 1] = tLen; 4 for(int i = tLen - 2; i >= 0; -- i) { 5 int j = i; 6 7 while(j >= 0 && T[j] == T[tLen - 1 - (i - j)]) 8 -- j; 9 10 bmGLenth[i] = i - j; 11 } 12 13 }
不難發現,此代碼的時間復雜度為O(n2),事實上我們可以優化一下,優化成O(n),
下見代碼:
為方便讀者理解,先說一下以下代碼的原理:
如果i所到的最遠處cur與pre不等,那么就存在最小循環節在[cur, pre]和[cur, tLen - 1]中,
這個最小循環節內部(指除去最右字符剩下的字符)的字符必然不是完全匹配(也就是從此
字符開始與后綴進行匹配,必然匹配不到一個循環節的長度),這些不完全匹配的字符所能
匹配的最大長度在每個循環節中必然相同;而完全匹配的字符(也就是最右字符)所能匹配
的最大長度的差值必然是循環節的整數倍。
不知道這樣說對不對,若有不對之處還請讀者指正,若在理還請讀者細細品味。
這個原理也僅是讀者通過代碼進行理解而得到的,至於原來最先想出此優化代碼的程序員已
不可考,其根本原理也已不可知。
1 void get_bmGLength(const string& T, int bmGLength[]) { 2 int tLen = T.size(); 3 bmGLength[tLen - 1] = tLen; 4 5 int cur = tLen - 1; // 保存當前下標i所到的最遠位置 6 int pre; // 保存下標i先前的位置 7 8 for(int i = tLen - 2; i >= 0; -- i) { 9 10 // i > cur 是因為i必須要曾經遍歷到過下標cur方可 11 // (tLen - 1 - pre)表示先前i到T尾字符的長度 12 // bmGLength[i + (tLen - 1 - pre)] < i - cur這個條件為什么要加上, 13 // 我也不明白,不過去掉這個條件,按照原理是成立的,加上也沒有錯,給出一個題目鏈接,讀者可以測試一下編者說的是否正確 14 // http://acm.hdu.edu.cn/showproblem.php?pid=1711 15 16 /* 網上源代碼: 17 if(i > cur && bmGLength[i + (tLen - 1 - pre)] < i - cur) { 18 bmGLength[i] = bmGLength[i + (tLen - 1 - pre)]; 19 continue; 20 } 21 */ 22 // 原理代碼: 23 if(i > cur) { 24 bmGLength[i] = bmGLength[i + (tLen - 1 - pre)]; 25 continue; 26 } 27 // i所到的最遠的位置必然是i最小時 28 cur = min(cur, i); 29 pre = i; 30 31 while(cur >= 0 && T[cur] == T[tLen - 1 - (pre - cur)]) 32 -- cur; 33 34 bmGLenth[i] = pre - cur; 35 } 36 37 } 38
OK,講解完畢,下面給出BM完整代碼:
1 void get_bmB(const string& T, int bmB[]) { 2 int tLen = T.size(); 3 for(int i = 0; i < MAXSIZE; ++ i) { 4 bmB[i] = tlen; 5 } 6 7 for(int i = 0; i < tLen; ++i) { 8 bmB[T[i]] = tLen - 1 - i; 9 } 10 } 11 12 void get_bmGLength(const string& T, int bmGLength[]) { 13 int tLen = T.size(); 14 bmGLength[tLen - 1] = tLen; 15 for(int i = tLen - 2; i >= 0; -- i) { 16 int j = i; 17 while(j >= 0 && T[j] == T[tLen - 1 - (i - j)]) 18 -- j; 19 20 bmGLenth[i] = i - j; 21 } 22 } 23 24 void get_bmGLength(const string& T, int bmGLength[]) { 25 int tLen = T.size(); 26 bmGLength[tLen - 1] = tLen; 27 28 int cur = tLen - 1; 29 int pre; 30 31 for(int i = tLen - 2; i >= 0; -- i) { 32 /* 網上源代碼: 33 if(i > cur && bmGLength[i + (tLen - 1 - pre)] < i - cur) { 34 bmGLength[i] = bmGLength[i + (tLen - 1 - pre)]; 35 continue; 36 } 37 */ 38 // 原理代碼: 39 if(i > cur) { 40 bmGLength[i] = bmGLength[i + (tLen - 1 - pre)]; 41 continue; 42 } 43 44 cur = min(cur, i); 45 pre = i; 46 while(cur >= 0 && T[cur] == T[tLen - 1 - (pre - cur)]) 47 -- cur; 48 bmGLenth[i] = pre - cur; 49 } 50 } 51 52 int bmGlength[N]; 53 54 void get_bmG(const string& T, int bmG[]) { 55 56 get_bmGLength(T, bmGLength); 57 58 int tLen = T.size(); 59 for(int i = 0; i < tLen; ++ i) { 60 bmG[i] = tLen; 61 } 62 63 int j = 0; 64 for(int i = tLen - 2; i >= 0; -- i) { 65 if(bmGLength[i] == i + 1) { 66 for(; j < tLen - 1 - i; ++ j) { 67 if(bmG[j] == tLen) 68 bmG[j] = tLen - 1 - i; 69 } 70 } 71 } 72 73 for(int i = 0; i < tLen - 1; ++ i) { 74 bmG[tLen - 1 - bmGLenth[i]] = tLen - 1 - i; 75 } 76 } 77 78 int BM(const string& S, const string& T, int bmG[], int bmB[]) { 79 80 get_bmB(T, bmB); 81 get_bmG(T, bmG); 82 83 int sLen = S.size() - T.size(); 84 int tLen = T.size(); 85 int i = 0; 86 87 while(i <= sLen) { 88 int j = tLen - 1; 89 for(; j > -1 && S[i + j] == T[j]; --j) ; 90 91 if(j == -1) 92 return i; 93 94 i += max(bmG[j], bmB[S[i + j]] - (tLen - 1 - j)); 95 } 96 return -1; 97 }
ok,BM算法介紹完畢,下面介紹Sundy算法,最簡單的算法。
四、Sunday算法
Sunday算法是Daniel M.Sunday於1990年提出的字符串模式匹配。相對比較KMP和BM
算法而言,簡單了許多。
原理與BM算法相仿,有點像其刪減版,所以其時間復雜度和BM算法差不多,平均性能的
時間復雜度也為O(n),最差情況的時間復雜度為O(n * m),但是要容易理解。
匹配原理:從前往后匹配,如果遇到不匹配情況判斷母串S參與匹配的最后一位的下一位字符
,如果該字符出現在模板串T中,選擇最右出現的位置進 行對齊;否則直接跳過該匹配區域。
原理看着都這么繁瑣,而且難懂,還是給讀者上圖吧:
母串S:
S E A R C H S U B S T R I N G
模板串T:
S U B S T R I N G
開始匹配:
S E A R C H S U B S T R I N G
↨
S U B S T R I N G
繼續下一字符匹配:
S E A R C H S U B S T R I N G
↨
S U B S T R I N G
出現不匹配情況,查找母串參與匹配的最后一位字符的下一字符,上圖中S中最后一位參與
匹配的字符是顏色為藍色的字符’B’,其下一字符為’S’,在T中,字符’S’出現兩次,按照原理,
選擇最右位置出現的’S’進行對齊,那么可以得到:
S E A R C H S U B S T R I N G
↨
S U B S T R I N G
直接跳過大片區域。
假設母串S為:
S E A R C H S U B Z T R I N G
那么當匹配到上述情況時,字符’Z’在T中沒有出現,那么就可以得到下面的情況:
S E A R C H S U B Z T R I N G
↨
S U B S T R I N G
跳過的區域很大吧。
ok,這就是其原理的兩種情況,很簡單吧,下面給出代碼解釋:
注:S表示母串,T表示模板串
1 int moveLength[MAXSIZE]; // 匹配不成功時的移動步長,默認初始化 2 3 int Sunday(const string& S, const string &T) { 4 getMoveLength(T); 5 6 int tLen = T.size(); 7 int sLen = S.size(); 8 int i = 0; // S遍歷下標 9 while(i < sLen) { 10 int j = 0; 11 // 符合條件下標就繼續右移 12 for( ; j < tLen && i + j < sLen && S[i + j] == T[j]; ++ j) ; 13 // 遍歷結束,判斷遍歷情況 14 if(j >= tLen) return i; 15 // 查找不成功,那么S下標右移 16 if(i + tLen >= sLen) 17 return -1; 18 i += moveLength[S[i + tLen]]; 19 } 20 21 return -1; 22 }
匹配過程很簡單,相信讀者很容易理解。
那么,需要求解moveLength數組,下面給出其求解代碼:
1 int MAXSIZE = 256; // 字符串種類數,視情況而定 2 3 void getMoveLength(const string &T) { 4 int tLen = T.size(); 5 // 默認S中的任何字符均不出現在T中,那么每次移動的距離為T的長度 + 1 6 for(int i = 0; i < MAXSIZE; ++ i) 7 moveLength[i] = tLen + 1; 8 9 // 查找能夠出現在T中的字符,若一個字符出現多次,選擇最右位置的字符,所以T的下標遍歷從0開始 10 for(int i = 0; T[i]; ++ i) 11 moveLength[T[i]] = tLen - i; 12 } 13
ok,代碼解釋完畢,非常簡單。
下面給出完整代碼:
1 int MAXSIZE = 256; 2 int moveLength[MAXSIZE]; 3 4 void getMoveLength(const string &T) { 5 int tLen = T.size(); 6 for(int i = 0; i < MAXSIZE; ++ i) 7 moveLength[i] = tLen + 1; 8 9 for(int i = 0; T[i]; ++ i) 10 moveLength[T[i]] = tLen - i; 11 } 12 13 int Sunday(const string& S, const string &T) { 14 getMoveLength(T); 15 16 int tLen = T.size(); 17 int sLen = S.size(); 18 int i = 0; 19 while(i < sLen) { 20 int j = 0; 21 for( ; j < tLen && i + j < sLen && S[i + j] == T[j]; ++ j) ; 22 23 if(j >= tLen) return i; 24 if(i + tLen > sLen) 25 return -1; 26 i += moveLength[S[i + tLen]]; 27 } 28 29 return -1; 30 }
BF、KMP、BM、Sunday算法均介紹完畢,若讀者有不明之處抑或博文有誤之類,請盡情留言,編者看到后會及時回復及修改博文。
希望讀者理解四種算法之后能夠自己獨立手寫其核心代碼,對讀者加深印象以及其核心原理很有裨益。
最后再次列舉推薦博文以及相關書籍(當然也曾讀到過一些博文以及書籍,從編者角度來說不易理解,便沒有推薦,還望見諒):
July博文(再次推薦) http://blog.csdn.net/v_july_v/article/details/7041827
算法導論(第三版),機械工業出版社,Thomas H. Cormen ... 著,殷建平...譯
暫時推薦上面比較稀少的博文與書籍,若有讀者能夠推薦給編者比較好的博文及書籍抑或日后看到,定會添加於上。
2014年11月5日