Boyer-Moore高質量實現代碼詳解與算法詳解
鑒於我見到對算法本身分析非常透徹的文章以及實現的非常精巧的文章,所以就轉載了,本文的貢獻在於將兩者結合起來,方便大家了解代碼實現!
C語言代碼實現轉自:
http://www-igm.univ-mlv.fr/~lecroq/string/node14.html
另外,網站http://www.cs.utexas.edu/users/moore/best-ideas/string-searching/fstrpos-example.html有個關於BM算法的詳細例子,看看挺好的。
BM算法的論文在這兒http://www.cs.utexas.edu/users/moore/publications/fstrpos.pdf
BM算法
后綴匹配,是指模式串的比較從右到左,模式串的移動也是從左到右的匹配過程,經典的BM算法其實是對后綴蠻力匹配算法的改進。所以還是先從最簡單的后綴蠻力匹配算法開始。下面直接給出偽代碼,注意這一行代碼:j++;BM算法所做的唯一的事情就是改進了這行代碼,即模式串不是每次移動一步,而是根據已經匹配的后綴信息,從而移動更多的距離。
1 j = 0; 2 3 while (j <= strlen(T) - strlen(P)) { 4 5 for (i = strlen(P) - 1; i >= 0 && P[i] ==T[i + j]; --i) 6 7 if (i < 0) 8 9 match; 10 11 else 12 13 j++; 14 15 }
為了實現更快移動模式串,BM算法定義了兩個規則,好后綴規則和壞字符規則,如下圖可以清晰的看出他們的含義。利用好后綴和壞字符可以大大加快模式串的移動距離,不是簡單的++j,而是j+=max (shift(好后綴), shift(壞字符))
先來看如何根據壞字符來移動模式串,shift(壞字符)分為兩種情況:
- 壞字符沒出現在模式串中,這時可以把模式串移動到壞字符的下一個字符,繼續比較,如下圖:
- 壞字符出現在模式串中,這時可以把模式串第一個出現的壞字符和母串的壞字符對齊,當然,這樣可能造成模式串倒退移動,如下圖:
此處配的圖是不准確的,因為顯然加粗的那個b並不是”最靠右的”b。而且也與下面給出的代碼沖突!我看了論文,論文的意思是最右邊的。當然了,盡管一時大意圖配錯了,論述還是沒有問題的,我們可以把圖改正一下,把圈圈中的b改為字母f就好了。接下來的圖就不再更改了,大家心里有數就好。
為了用代碼來描述上述的兩種情況,設計一個數組bmBc['k'],表示壞字符‘k’在模式串中出現的位置距離模式串末尾的最大長度,那么當遇到壞字符的時候,模式串可以移動距離為: shift(壞字符) = bmBc[T[i]]-(m-1-i)。如下圖:
數組bmBc的創建非常簡單,直接貼出代碼如下:
1 void preBmBc(char *x, int m, int bmBc[]) { 2 3 int i; 4 5 for (i = 0; i < ASIZE; ++i) 6 7 bmBc[i] = m; 8 9 for (i = 0; i < m - 1; ++i) 10 11 bmBc[x[i]] = m - i - 1; 12 13 }
代碼分析:
- ASIZE是指字符種類個數,為了方便起見,就直接把ASCII表中的256個字符全表示了,哈哈,這樣就不會漏掉哪個字符了。
- 第一個for循環處理上述的第一種情況,這種情況比較容易理解就不多提了。
第二個for循環,bmBc[x[i]]中x[i]表示模式串中的第i個字符。
bmBc[x[i]] = m - i - 1;也就是計算x[i]這個字符到串尾部的距離。
- 為什么第二個for循環中,i從小到大的順序計算呢?哈哈,技巧就在這兒了,原因在於就可以在同一字符多次出現的時候以最靠右的那個字符到尾部距離為最終的距離。當然了,如果沒在模式串中出現的字符,其距離就是m了。
再來看如何根據好后綴規則移動模式串,shift(好后綴)分為三種情況:
- 模式串中有子串匹配上好后綴,此時移動模式串,讓該子串和好后綴對齊即可,如果超過一個子串匹配上好后綴,則選擇最靠左邊的子串對齊。
- 模式串中沒有子串匹配上后后綴,此時需要尋找模式串的一個最長前綴,並讓該前綴等於好后綴的后綴,尋找到該前綴后,讓該前綴和好后綴對齊即可。
- 模式串中沒有子串匹配上后后綴,並且在模式串中找不到最長前綴,讓該前綴等於好后綴的后綴。此時,直接移動模式到好后綴的下一個字符。
為了實現好后綴規則,需要定義一個數組suffix[],其中suffix[i] = s 表示以i為邊界,與模式串后綴匹配的最大長度,如下圖所示,用公式可以描述:滿足P[i-s, i] == P[m-s, m]的最大長度s。
構建suffix數組的代碼如下:
1 void suffixes(char *x, int m, int *suff) 2 { 3 suff[m-1]=m; 4 for (i=m-2;i>=0;--i){ 5 q=i; 6 while(q>=0&&x[q]==x[m-1-i+q]) 7 --q; 8 suff[i]=i-q; 9 } 10 }
注解:這一部分代碼乏善可陳,都是常規代碼,這里就不多說了。
有了suffix數組,就可以定義bmGs[]數組,bmGs[i] 表示遇到好后綴時,模式串應該移動的距離,其中i表示好后綴前面一個字符的位置(也就是壞字符的位置),構建bmGs數組分為三種情況,分別對應上述的移動模式串的三種情況
- 模式串中有子串匹配上好后綴
- 模式串中沒有子串匹配上好后綴,但找到一個最大前綴
- 模式串中沒有子串匹配上好后綴,但找不到一個最大前綴
構建bmGs數組的代碼如下:
1 void preBmGs(char *x, int m, int bmGs[]) { 2 int i, j, suff[XSIZE]; 3 suffixes(x, m, suff); 4 for (i = 0; i < m; ++i) 5 bmGs[i] = m; 6 j = 0; 7 for (i = m - 1; i >= 0; --i) 8 if (suff[i] == i + 1) 9 for (; j < m - 1 - i; ++j) 10 if (bmGs[j] == m) 11 bmGs[j] = m - 1 - i; 12 for (i = 0; i <= m - 2; ++i) 13 bmGs[m - 1 - suff[i]] = m - 1 - i; 14 }
注解:
這一部分代碼挺有講究,寫的很巧妙,這里談談我的理解。講解代碼時候是分為三種情況來說明的,其實第二種和第三種可以合並,因為第三種情況相當於與好后綴匹配的最長前綴長度為0。
由於我們的目的是獲得精確的bmGs[i],故而若一個字符同時符合上述三種情況中的幾種,那么我們選取最小的bmGs[i]。比如當模式傳中既有子串可以匹配上好后串,又有前綴可以匹配好后串的后串,那么此時我們應該按照前者來移動模式串,也就是bmGs[i]較小的那種情況。故而每次修改bmGs[i]都應該使其變小,記住這一點,很重要!
而在這三種情況中第三種情況獲得的bmGs[i]值大於第二種大於第一種。故而寫代碼的時候我們先計算第三種情況,再計算第二種情況,再計算第一種情況。為什么呢,因為對於同一個位置的多次修改只會使得bmGs[i]越來越小。
- 代碼4-5行對應了第三種情況,7-11行對於第二種情況,12-13對應第三種情況。
- 第三種情況比較簡單直接賦值m,這里就不多提了。
- 第二種情況有點意思,咱們細細的來品味一下。
1. 為什么從后往前,也就是i從大到小?
原因在於如果i,j(i>j)位置同時滿足第二種情況,那么m-1-i<m-1-j,而第十行代碼保證了每個位置最多只能被修改一次,故而應該賦值為m-1-i,這也說明了為什么要 從后往前計算。
2. 第8行代碼的意思是找到了合適的位置,為什么這么說呢?
因為根據suff的定義,我們知道
x[i+1-suff[i]…i]==x[m-1-siff[i]…m-1],而suff[i]==i+1,我們知道x[i+1-suff[i]…i]=x[0,i],也就是前綴,滿足第二種情況。
3. 第9-11行就是在對滿足第二種情況下的賦值了。第十行確保了每個位置最多只能被修改一次。
- 第12-13行就是處理第一種情況了。為什么順序從前到后呢,也就是i從小到大?
原因在於如果suff[i]==suff[j],i<j,那么m-1-i>m-1-j,我們應該取后者作為bmGs[m - 1 - suff[i]]的值。
再來重寫一遍BM算法:
1 void BM(char *x, int m, char *y, int n) { 2 int i, j, bmGs[XSIZE], bmBc[ASIZE]; 3 4 /* Preprocessing */ 5 preBmGs(x, m, bmGs); 6 preBmBc(x, m, bmBc); 7 8 /* Searching */ 9 j = 0; 10 while (j <= n - m) { 11 for (i = m - 1; i >= 0 && x[i] == y[i + j]; --i); 12 if (i < 0) { 13 OUTPUT(j); 14 j += bmGs[0]; 15 } 16 else 17 j += MAX(bmGs[i], bmBc[y[i + j]] - m + 1 + i); 18 } 19 }