這篇長文歷時近兩天終於完成了,前兩天幫網站翻譯一篇文章「爲何GNU grep如此之快?」,裏面說起到grep速度快的一個重要緣由是使用了Boyer-Moore算法做爲字符串搜索算法,興趣之下就想了解這個算法,發現這個算法一開始還挺難理解的,也許是我理解能力不是很好吧,花了小半天才看懂,看懂了事後就想分享下,由於以爲這個算法真的挺不錯的,之前一直覺得字符串搜索算法中KMP算很不錯的了,沒想到還有更好的,Boyer-Moore算法平均要比KMP快3-5倍。html
下面是我對該算法的理解,參考了一些關於該算法的介紹,裏面每一張圖都畫的很認真,但願能講清楚問題,有什麼錯誤、疑問或不懂的地方麻煩你們必定要提出來,共同窗習進步!下面正文開始。node
在用於查找子字符串的算法當中,BM(Boyer-Moore)算法是目前被認爲最高效的字符串搜索算法,它由Bob Boyer和J Strother Moore設計於1977年。 通常狀況下,比KMP算法快3-5倍。該算法經常使用於文本編輯器中的搜索匹配功能,好比你們所熟知的GNU grep命令使用的就是該算法,這也是GNU grep比BSD grep快的一個重要緣由,具體推薦看下我最近的一篇譯文「爲何GNU grep如此之快?」做者是GNU grep的編寫者Mike Haertel。算法
假設文本串text長度爲n,模式串pattern長度爲m,BM算法的主要特徵爲:
數組
- 從右往左進行比較匹配(通常的字符串搜索算法如KMP都是從從左往右進行匹配);框架
- 算法分爲兩個階段:預處理階段和搜索階段;編輯器
- 預處理階段時間和空間複雜度都是是O(m+),
是字符集大小,通常爲256;
性能
- 搜索階段時間複雜度是O(mn);
學習
- 當模式串是非週期性的,在最壞的狀況下算法須要進行3n次字符比較操做;
網站
- 算法在最好的狀況下達到O(n / m),好比在文本串bn中搜索模式串am-1b ,只須要n/m次比較。
spa
這些特徵先讓你們對該算法有個基本的瞭解,等看懂了算法再來看這些特徵又會有些額外的收穫。
常規的匹配算法移動模式串的時候是從左到右,而進行比較的時候也是從左到右的,基本框架是:
j = 0; while(j <= strlen(text) - strlen(pattern)){ for (i = 0; i < strlen(pattern) && pattern[i] == text[i + j]; ++i); if (i == strlen(pattern)) { Match; break; } else ++j; }
而BM算法在移動模式串的時候是從左到右,而進行比較的時候是從右到左的,基本框架是:
j = 0; while(j <= strlen(text) - strlen(pattern)){ for (i = strlen(pattern); i >= 0 && pattern[i] == text[i + j]; --i); if (i < 0)) { Match; break; } else j += BM(); }
BM算法的精華就在於BM(text, pattern),也就是BM算法當不匹配的時候一次性能夠跳過不止一個字符。即它不須要對被搜索的字符串中的字符進行逐一比較,而會跳過其中某些部分。一般搜索關鍵字越長,算法速度越快。它的效率來自於這樣的事實:對於每一次失敗的匹配嘗試,算法都可以使用這些信息來排除儘量多的沒法匹配的位置。即它充分利用待搜索字符串的一些特徵,加快了搜索的步驟。
BM算法實際上包含兩個並行的算法(也就是兩個啓發策略):壞字符算法(bad-character shift)和好後綴算法(good-suffix shift)。這兩種算法的目的就是讓模式串每次向右移動儘量大的距離(即上面的BM()儘量大)。
下面不直接書面解釋這兩個算法,爲了更加通俗易懂,先用實例說明吧,這是最容易接受的方式。
你們來頭腦風暴下:如何加快字符串搜索?舉個很簡單的例子,以下圖所示,navie表示通常作法,逐個進行比對,從右向左,最後一個字符c與text中的d不匹配,pattern右移一位。但你們看一下這個d有什麼特徵?pattern中沒有d,所以你無論右移一、二、三、4位確定仍是不匹配,何須花這個功夫呢?直接右移5(strlen(pattern))位再進行比對不是更好嗎?好,就這樣作,右移5位後,text中的b與pattern中的c比較,發現仍是不一樣,這時咋辦?b在pattern中有因此不能一下右移5位了,難道直接右移一位嗎?No,能夠直接將pattern中的b右移到text中b的位置進行比對,可是pattern中有兩個b,右移哪一個b呢?保險的辦法是用最右邊的b與text進行比對,爲啥?下圖說的很清楚了,用最左邊的b太激進了,容易漏掉真正的匹配,圖中用最右邊的b後發現正好全部的都匹配成功了,若是用最左邊的不就錯過了這個匹配項嗎?這個啓發式搜索就是BM算法作的。
圖1
But, 若是遇到下面這樣的狀況,開始pattern中的c和text中的b不匹配,Ok,按上面的規則將pattern右移直至最右邊的b與text的b對齊進行比對。再將pattern中的c與text中的c進行比對,匹配繼續往左比對,直到位置3處pattern中的a與text中的b不匹配了,按上面講的啓發式規則應該將pattern中最右邊的b與text的b對齊,可這時發現啥了?pattern走了回頭路,幹嘛?固然不幹,纔不要那麼傻,針對這種狀況,只須要將pattern簡單的右移一步便可,堅持不走回頭路!
圖2
好了,這就是所謂的「壞字符算法」,簡單吧,通俗易懂吧,上面用紅色粗體字標註出來的b就是「壞字符」,即不匹配的字符,壞字符是針對text的。
BM難道就這麼簡單?就一個啓發式規則就搞定了?固然不是了,你們再次頭腦風暴一下,有沒有其餘加快字符串搜索的方法呢?好比下面的例子
圖3
一開始利用了壞字符算法一下移了4位,不錯,接下來遇到了回頭路,沒辦法只能保守移一位,但真的就只能移一位嗎?No,由於pattern中前面其餘位置也有剛剛匹配成功的後綴ab,那麼將pattern前面的ab右移到text剛匹配成功的ab對齊繼續往前匹配不是更好嗎?這樣就能夠一次性右移兩位了,很好的有一個啓發式搜索規則啊。有人可能想:要是前面沒已經匹配成功的後綴咋辦?是否是就無效了?不徹底是,這要看狀況了,好比下面這個例子。
圖4
cbab這個後綴已經成功匹配,而後b沒成功,而pattern前面也沒發現cbab這樣的串,這樣就直接保守移一位?No,前面有ab啊,這是cbab後綴的一部分,也能夠好好利用,直接將pattern前面的ab右移到text已經匹配成功的ab位置處繼續往前匹配,這樣一會兒就右移了四位,很好。固然,若是前面徹底沒已經匹配成功的後綴或部分後綴,好比最前面的babac,那就真的不能利用了。
好了,這就是所謂的「好後綴算法」,簡單吧,通俗易懂吧,上面用紅色字標註出來的ab(前面例子)和cbab(上面例子)就是「好後綴」,好後綴是針對pattern的。
下面,最後再舉個例子說明啥是壞字符,啥是好後綴。
主串 : mahtavaatalomaisema omalomailuun
模式串: maisemaomaloma
壞字符:主串中的「t」爲壞字符。
好後綴:模式串中的aloma爲「好後綴」。
BM就這麼簡單?是的,容易理解但並非每一個人都能想到的兩個啓發式搜索規則就造就了BM這樣一個優秀的算法。那麼又有個問題?這兩個算法怎麼運用,一下壞字符的,一下好後綴的,何時該用壞字符?何時該用好後綴呢?很好的問題,這就要看哪一個右移的位數多了,好比上面的例子,一開始若是用好後綴的話只能移一位而用壞字符就能右移三位,此時固然選擇壞字符算法了。接下來若是繼續用壞字符則只能右移一位而用好後綴就能一下右移四位,這時候你說用啥呢?So,這兩個算法是「並行」的,哪一個大用哪一個。
光用例子說明固然不夠,太淺了,並且還不必定能徹底覆蓋全部狀況,不精確。下面就開始真正的理論探討了。
(1)壞字符算法
當出現一個壞字符時, BM算法向右移動模式串, 讓模式串中最靠右的對應字符與壞字符相對,而後繼續匹配。壞字符算法有兩種狀況。
Case1:模式串中有對應的壞字符時,讓模式串中最靠右的對應字符與壞字符相對(PS:BM不可能走回頭路,由於如果回頭路,則移動距離就是負數了,確定不是最大移動步數了),以下圖。
Case2:模式串中不存在壞字符,很好,直接右移整個模式串長度這麼大步數,以下圖。
(2)好後綴算法
若是程序匹配了一個好後綴, 而且在模式中還有另一個相同的後綴或後綴的部分, 那把下一個後綴或部分移動到當先後綴位置。假如說,pattern的後u個字符和text都已經匹配了,可是接下來的一個字符不匹配,我須要移動才能匹配。若是說後u個字符在pattern其餘位置也出現過或部分出現,咱們將pattern右移到前面的u個字符或部分和最後的u個字符或部分相同,若是說後u個字符在pattern其餘位置徹底沒有出現,很好,直接右移整個pattern。這樣,好後綴算法有三種狀況,以下圖所示:
Case1:模式串中有子串和好後綴徹底匹配,則將最靠右的那個子串移動到好後綴的位置繼續進行匹配。
Case2:若是不存在和好後綴徹底匹配的子串,則在好後綴中找到具備以下特徵的最長子串,使得P[m-s…m]=P[0…s]。
Case3:若是徹底不存在和好後綴匹配的子串,則右移整個模式串。
(3)移動規則
BM算法的移動規則是:
將3中算法基本框架中的j += BM(),換成j += MAX(shift(好後綴),shift(壞字符)),即
BM算法是每次向右移動模式串的距離是,按照好後綴算法和壞字符算法計算獲得的最大值。
shift(好後綴)和shift(壞字符)經過模式串的預處理數組的簡單計算獲得。壞字符算法的預處理數組是bmBc[],好後綴算法的預處理數組是bmGs[]。
BM算法子串比較失配時,按壞字符算法計算pattern須要右移的距離,要藉助bmBc數組,而按好後綴算法計算pattern右移的距離則要藉助bmGs數組。下面講下怎麼計算bmBc[]和bmGs[]這兩個預處理數組。
(1)計算壞字符數組bmBc[]
這個計算應該很容易,彷佛只須要bmBc[i] = m - 1 - i就好了,但這樣是不對的,由於i位置處的字符可能在pattern中多處出現(以下圖所示),而咱們須要的是最右邊的位置,這樣就須要每次循環判斷了,很是麻煩,性能差。這裏有個小技巧,就是使用字符做爲下標而不是位置數字做爲下標。這樣只須要遍歷一遍便可,這貌似是空間換時間的作法,但若是是純8位字符也只須要256個空間大小,並且對於大模式,可能自己長度就超過了256,因此這樣作是值得的(這也是爲何數據越大,BM算法越高效的緣由之一)。
如前所述,bmBc[]的計算分兩種狀況,與前一一對應。
Case1:字符在模式串中有出現,bmBc['v']表示字符v在模式串中最後一次出現的位置,距離模式串串尾的長度,如上圖所示。
Case2:字符在模式串中沒有出現,如模式串中沒有字符v,則BmBc['v'] = strlen(pattern)。
寫成代碼也很是簡單:
void PreBmBc(char *pattern, int m, int bmBc[]) { int i; for(i = 0; i < 256; i++) { bmBc[i] = m; } for(i = 0; i < m - 1; i++) { bmBc[pattern[i]] = m - 1 - i; } }
計算pattern須要右移的距離,要藉助bmBc數組,那麼bmBc的值是否是就是pattern實際要右移的距離呢?No,想一想也不是,好比前面舉例說到利用bmBc算法還可能走回頭路,也就是右移的距離是負數,而bmBc的值絕對不多是負數,因此二者不相等。那麼pattern實際右移的距離怎麼算呢?這個就要看text中壞字符的位置了,前面說過壞字符算法是針對text的,仍是看圖吧,一目瞭然。圖中v是text中的壞字符(對應位置i+j),在pattern中對應不匹配的位置爲i,那麼pattern實際要右移的距離就是:bmBc['v'] - m + 1 + i。
(2)計算好後綴數組bmGs[]
這裏bmGs[]的下標是數字而不是字符了,表示字符在pattern中位置。
如前所述,bmGs數組的計算分三種狀況,與前一一對應。假設圖中好後綴長度用數組suff[]表示。
Case1:對應好後綴算法case1,以下圖,j是好後綴以前的那個位置。
Case2:對應好後綴算法case2:以下圖所示:
Case3:對應與好後綴算法case3,bmGs[i] = strlen(pattern)= m
這樣就清晰了,代碼編寫也比較簡單:
void PreBmGs(char *pattern, int m, int bmGs[]) { int i, j; int suff[SIZE]; // 計算後綴數組 suffix(pattern, m, suff); // 先所有賦值爲m,包含Case3 for(i = 0; i < m; i++) { bmGs[i] = m; } // Case2 j = 0; for(i = m - 1; i >= 0; i--) { if(suff[i] == i + 1) { for(; j < m - 1 - i; j++) { if(bmGs[j] == m) bmGs[j] = m - 1 - i; } } } // Case1 for(i = 0; i <= m - 2; i++) { bmGs[m - 1 - suff[i]] = m - 1 - i; } }
So easy? 結束了嗎?還差一步呢,這裏的suff[]咋求呢?
在計算bmGc數組時,爲提升效率,先計算輔助數組suff[]表示好後綴的長度。
suff數組的定義:m是pattern的長度
看上去有些晦澀難懂,實際上suff[i]就是求pattern中以i位置字符爲後綴和以最後一個字符爲後綴的公共後綴串的長度。不知道這樣說清楚了沒有,仍是舉個例子吧:
i : 0 1 2 3 4 5 6 7
pattern: b c a b a b a b
當i=7時,按定義suff[7] = strlen(pattern) = 8
當i=6時,以pattern[6]爲後綴的後綴串爲bcababa,以最後一個字符b爲後綴的後綴串爲bcababab,二者沒有公共後綴串,因此suff[6] = 0
當i=5時,以pattern[5]爲後綴的後綴串爲bcabab,以最後一個字符b爲後綴的後綴串爲bcababab,二者的公共後綴串爲abab,因此suff[5] = 4
以此類推……
當i=0時,以pattern[0]爲後綴的後綴串爲b,以最後一個字符b爲後綴的後綴串爲bcababab,二者的公共後綴串爲b,因此suff[0] = 1
這樣看來代碼也很好寫:
void suffix(char *pattern, int m, int suff[]) { int i, j; int k; suff[m - 1] = m; for(i = m - 2; i >= 0; i--) { j = i; while(j >= 0 && pattern[j] == pattern[m - 1 - i + j]) j--; suff[i] = i - j; } }
這樣可能就萬事大吉了,但是總有人對這個算法不滿意,感受太暴力了,因而有聰明人想出一種方法,對上述常規方法進行改進。基本的掃描都是從右向左,改進的地方就是利用了已經計算獲得的suff[]值,計算如今正在計算的suff[]值。具體怎麼利用,看下圖:
i是當前正準備計算suff[]值的那個位置。
f是上一個成功進行匹配的起始位置(不是每一個位置都能進行成功匹配的, 實際上可以進行成功匹配的位置並很少)。
g是上一次進行成功匹配的失配位置。
若是i在g和f之間,那麼必定有P[i]=P[m-1-f+i];而且若是suff[m-1-f+i] < i-g, 則suff[i] = suff[m-1-f+i],這不就利用了前面的suff了嗎。
PS:這裏有些人可能以爲應該是suff[m-1-f+i] <= i - g,由於若suff[m-1-f+i] = i - g,仍是沒超過suff[f]的範圍,依然能夠利用前面的suff[],但這是錯誤的,好比一個極端的例子:
i :0 1 2 3 4 5 6 7 8 9
pattern:a a a a a b a a a a
suff[4] = 4,這裏f=4,g=0,當i=3是,這時suff[m-1=f+i]=suff[8]=3,而suff[3]=4,二者不相等,由於上一次的失配位置g可能會在此次獲得匹配。
好了,這樣解釋事後,代碼也比較簡單:
void suffix(char *pattern, int m, int suff[]) { int f, g, i; suff[m - 1] = m; g = m - 1; for (i = m - 2; i >= 0; --i) { if (i > g && suff[i + m - 1 - f] < i - g) suff[i] = suff[i + m - 1 - f]; else { if (i < g) g = i; f = i; while (g >= 0 && pattern[g] == pattern[g + m - 1 - f]) --g; suff[i] = f - g; } } }
結束了?OK,能夠說重要的算法都完成了,但願你們可以看懂,爲了驗證你們到底有沒有徹底看明白,下面出個簡單的例子,你們算一下bmBc[]、suff[]和bmGs[]吧。
舉例以下:
PS:這裏也許有人會問:bmBc['b']怎麼等於2,它不是最後出如今pattern最後一個位置嗎?按定義應該是0啊。請你們仔細看下bmBc的算法:
for(i = 0; i < m - 1; i++) { bmBc[pattern[i]] = m - 1 - i; }
這裏是i < m - 1不是i < m,也就是最後一個字符若是沒有在前面出現過,那麼它的bmBc值爲m。爲何最後一位不計算在bmBc中呢?很容易想啊,若是記在內該字符的bmBc就是0,按前所述,pattern須要右移的距離bmBc['v']-m+1+i=-m+1+i <= 0,也就是原地不動或走回頭路,固然不幹了,前面這種狀況已經說的很清楚了,因此這裏是m-1。
好了,全部的終於都講完了,下面整合一下這些算法吧。
#include <stdio.h> #include <string.h> #define MAX_CHAR 256 #define SIZE 256 #define MAX(x, y) (x) > (y) ? (x) : (y) void BoyerMoore(char *pattern, int m, char *text, int n); int main() { char text[256], pattern[256]; while(1) { scanf("%s%s", text, pattern); if(text == 0 || pattern == 0) break; BoyerMoore(pattern, strlen(pattern), text, strlen(text)); printf("\n"); } return 0; } void print(int *array, int n, char *arrayName) { int i; printf("%s: ", arrayName); for(i = 0; i < n; i++) { printf("%d ", array[i]); } printf("\n"); } void PreBmBc(char *pattern, int m, int bmBc[]) { int i; for(i = 0; i < MAX_CHAR; i++) { bmBc[i] = m; } for(i = 0; i < m - 1; i++) { bmBc[pattern[i]] = m - 1 - i; } /* printf("bmBc[]: "); for(i = 0; i < m; i++) { printf("%d ", bmBc[pattern[i]]); } printf("\n"); */ } void suffix_old(char *pattern, int m, int suff[]) { int i, j; suff[m - 1] = m; for(i = m - 2; i >= 0; i--) { j = i; while(j >= 0 && pattern[j] == pattern[m - 1 - i + j]) j--; suff[i] = i - j; } } void suffix(char *pattern, int m, int suff[]) { int f, g, i; suff[m - 1] = m; g = m - 1; for (i = m - 2; i >= 0; --i) { if (i > g && suff[i + m - 1 - f] < i - g) suff[i] = suff[i + m - 1 - f]; else { if (i < g) g = i; f = i; while (g >= 0 && pattern[g] == pattern[g + m - 1 - f]) --g; suff[i] = f - g; } } // print(suff, m, "suff[]"); } void PreBmGs(char *pattern, int m, int bmGs[]) { int i, j; int suff[SIZE]; // 計算後綴數組 suffix(pattern, m, suff); // 先所有賦值爲m,包含Case3 for(i = 0; i < m; i++) { bmGs[i] = m; } // Case2 j = 0; for(i = m - 1; i >= 0; i--) { if(suff[i] == i + 1) { for(; j < m - 1 - i; j++) { if(bmGs[j] == m) bmGs[j] = m - 1 - i; } } } // Case1 for(i = 0; i <= m - 2; i++) { bmGs[m - 1 - suff[i]] = m - 1 - i; } // print(bmGs, m, "bmGs[]"); } void BoyerMoore(char *pattern, int m, char *text, int n) { int i, j, bmBc[MAX_CHAR], bmGs[SIZE]; // Preprocessing PreBmBc(pattern, m, bmBc); PreBmGs(pattern, m, bmGs); // Searching j = 0; while(j <= n - m) { for(i = m - 1; i >= 0 && pattern[i] == text[i + j]; i--); if(i < 0) { printf("Find it, the position is %d\n", j); j += bmGs[0]; return; } else { j += MAX(bmBc[text[i + j]] - m + 1 + i, bmGs[i]); } } printf("No find.\n"); }
運行效果以下:
2. wiki: Boyer–Moore string search algorithm