出處:http://blog.csdn.net/appleprince88/article/details/11881323
一.簡述
在當前用於查找子字符串的算法中,BM(Boyer-Moore)算法是當前有效且應用比較廣的一中算法,各種文本編輯器的“查找”功能(Ctrl+F),大多采用Boyer-Moore算法。比我們在學習的KMP算法快3~5倍。
Boyer-Moore算法不僅效率高,而且構思巧妙,容易理解。1977年,德克薩斯大學的Robert S. Boyer教授和J Strother Moore教授發明了這種算法。
二.算法思想
我們知道常規的字符串匹配算法是從左往右的,這也比較符合我們一貫的思維,但是BM算法是從右往左的。一般匹配我們用的是蠻力匹配,而經典的BM算法其實是對後綴蠻力匹配算法的改進,下面我們給出蠻力後綴匹配的僞代碼。
- j = 0;
- while (j <= strlen(T) - strlen(P)) {
- for (i = strlen(P) - 1; i >= 0 && P[i] ==T[i + j]; --i)
- if (i < =0)
- match;
- else
- ++j;
- }
從上面的僞代碼中我們可以看出每當失匹的時候,就會往後移一位,也就是上面++j這一行代碼;而BM算法所做的就是改進這一行代碼,即模式串不在每次只移動一步,而是根據已經匹配的後綴信息,來判斷移動的距離,通常80%左右能夠移動模式串的長度,從而可以跳過大量不必須比較的字符,大大提高了查找效率。
爲了實現更快的移動模式串,BM定義了兩個規則,壞後綴規則和好後綴規則。這兩個規則分別計算我們能夠向後移動模式串長度,然後選取這兩個規則中移動大的,作爲我們真正移動的距離。也就是上述僞代碼中j不在每次加一,而是加上上面兩個規則中移動長度大的。
假定字符串爲”HERE IS A SIMPLE EXAMPLE”,模式串爲”EXAMPLE”。下面我將闡述幾個概念,壞字符和好後綴。
上圖中我們看到,”S”與”E”不匹配。這時,“S”就被稱爲”壞字符”(bad character),即不匹配的字符。
上圖中 ”MPLE”與”MPLE”匹配。我們把這種情況稱爲”好後綴”(good suffix),即所有尾部匹配的字符串。注意,”MPLE”、”PLE”、”LE”、”E”都是好後綴,這點後面我們會用到。
壞字符算法
當出現一個壞字符時, BM算法向右移動模式串, 讓模式串中最靠右的對應字符與壞字符相對,然後繼續匹配。壞字符算法有兩種情況。
- 模式串中有對應的壞字符時,y爲字符串,x爲模式串,見下圖
壞字符出現在模式串中,這時可以把模式串第一個出現的壞字符和母串的壞字符對齊,也就是上面所說的最靠右。當然,這樣可能造成模式串倒退移動,因爲壞字符可能出現在與模式串失匹位置的右面,不過由於我們移動不光看壞後綴還看好後綴,所以不會後退。
- 模式串中不存在壞字符,這時可以把模式串移動到壞字符的下一個字符,繼續比較。如下圖所示
好後綴算法
好後綴算法分爲三種情況
- 模式串中有子串匹配上好後綴,此時移動模式串,讓該子串和好後綴對齊即可,如果超過一個子串匹配上好後綴,則選擇最靠右邊的子串對齊,防止有漏匹配的。
- 模式串中沒有子串匹配上後後綴,此時需要尋找模式串的一個最長前綴,並讓該前綴等於好後綴的後綴,尋找到該前綴後,讓該前綴和好後綴對齊即可。
- 模式串中沒有子串匹配上後後綴,並且在模式串中找不到最長前綴,讓該前綴等於好後綴的後綴。此時,直接移動模式到好後綴的下一個字符。
BM算法的大體思想到這裏我們基本介紹結束了,下面我們通過一個例子來具體感受一下
三.例子
下面,我根據Moore教授自己的例子來解釋這種算法。希望通過例子大家能有一個感性的認識。
1.
假定字符串爲”HERE IS ASIMPLE EXAMPLE”,搜索詞爲”EXAMPLE”。搜索詞我們下面都稱爲模式串。
2.
首先,”字符串”與”模式串”頭部對齊,從尾部開始比較。
我們看到,”S”與”E”不匹配。這時,“S”就被稱爲”壞字符”(badcharacter),即不匹配的字符。我們還發現,”S”不包含在模式串”EXAMPLE”之中,這意味着可以把模式串直接移到”S”的後一位。這裏適用壞規則。
3.
依然從尾部開始比較,發現”P”與”E”不匹配,所以”P”是”壞字符”。但是,”P”包含在模式串”EXAMPLE”之中。所以,根據壞規則將模式串中的最右的“P”與字符串中的”P”對齊,模式串後移兩位。
4.
我們由此總結出“壞字符規則”:
後移位數 = 壞字符的位置 – 搜索詞中的上一次出現位置
如果”壞字符”不包含在搜索詞之中,則上一次出現位置爲 -1。
以”P”爲例,它作爲”壞字符”,出現在搜索詞的第6位(從0開始編號),在搜索詞中的上一次出現位置爲4,所以後移 6 – 4 = 2位。再以前面第二步的”S”爲例,它出現在第6位,上一次出現位置是 -1(即未出現),則整個搜索詞後移 6 – (-1) = 7位。
5.
E和E匹配,繼續匹配
比較前一位,LE和LE匹配
比較前一位,PLE與PLE匹配
比較前面一位,”MPLE”與”MPLE”匹配。我們把這種情況稱爲”好後綴”(good suffix),即所有尾部匹配的字符串。注意,”MPLE”、”PLE”、”LE”、”E”都是好後綴。
6.
比較前一位,發現”I”與”A”不匹配。所以,”I”是”壞字符”。根據”壞字符規則”,此時模式串應該後移 2 –(-1)= 3 位。問題是,此時有沒有更好的移法?
我們知道,此時存在”好後綴”。所以,可以採用“好後綴規則”:
後移位數 = 好後綴的位置 – 模式串中的上一次出現位置
計算時,位置的取值以”好後綴”的最後一個字符爲準。如果”好後綴”在模式串中沒有重複出現,則它的上一次出現位置爲 -1。
所有的”好後綴”(MPLE、PLE、LE、E)之中,只有”E”在”EXAMPLE”之中出現兩次,所以後移 6 – 0 = 6位。取壞規則和好規則的最大的那個值,也就是我們要後移6位。後移後如下圖所示。
7.
繼續從尾部開始比較,”P”與”E”不匹配,因此”P”是”壞字符”。根據”壞字符規則”,後移 6 – 4 = 2位。
8.
從尾部開始逐位比較,發現全部匹配,於是搜索結束。如果還要繼續查找(即找出全部匹配),則根據”好後綴規則”,後移 6 – 0 = 6位,即頭部的”E”移到尾部的”E”的位置。
更多的例子,點這裏。
四.算法詳解
通過了一個例子後,相信大家對這個算法基本瞭解了,那麼下面我們將通過具體實現來深入解釋算法的一些細節。具體實現與上面那個例子稍微有點不一樣,但原理是一樣的。
首先我們要設計一個數組bmBc[],比如說bmBc[‘K’]表示壞字符‘k’在模式串中最右出現的位置距離模式串末尾的長度,那麼當遇到壞字符的時候,模式串可以移動距離爲: shift(壞字符) = bmBc[T[i]]-(m-1-i) (其中T[i]指的是在i位置上壞字符,(m-1-i)指的是壞字符位置到模式串末尾的長度),這個移動的距離與我們上面例子討論的移動距離的方式雖然不一樣,但原理是一樣的,都是求壞字符位置與在模式串出現壞字符位置的距離,當然這個距離有可能是負的,但是沒關係,遇到這種情況模式串就直接向後一位,重新開始匹配,但是由於有好後綴規則,我們選取大的進行移動,所以也可以不處理。如下圖:
數組bmBc的創建非常簡單,直接貼出僞代碼代碼如下:
- void preBmBc(char *x, int m, int bmBc[]) {
- int i;
- for (i = 0; i <ASIZE; ++i)
- bmBc[i] = m;
- for (i = 0; i < m - 1; ++i)
- bmBc[x[i]] = m - i - 1;
- }
上面ASIZE爲該字符集詞的個數,但是上述僞代碼存在一個問題如果是像中文這樣字符集的話會是bmBc數組非常大,所以我實現採用瞭如下的方法,就是通過鍵值對來取代數組,然後用一個專門的函數來查看鍵值對的值,如果存在就返回相應的值,不存在就返回模式串的長度。具體看代碼
- private void preBmBc(String pattern,int patLength,Map<String,Integer> bmBc)
- {
- System.out.println("bmbc start process...");
- for(int i=patLength-2;i>=0;i--)
- {
- if(!bmBc.containsKey(String.valueOf(pattern.charAt(i))))
- { bmBc.put(String.valueOf(pattern.charAt(i)),(Integer)(patLength-i-1));
- }
- }
- }
- private int getBmBc(String c,Map<String,Integer> bmBc,int m)
- {
- //如果在規則中則返回相應的值,否則返回pattern的長度
- if(bmBc.containsKey(c))
- {
- return bmBc.get(c);
- }else
- {
- return m;
- }
- }
爲了實現好後綴規則,需要定義一個數組suffix[],其中suffix[i] = s 表示以i爲邊界,與模式串後綴匹配的最大長度,如下圖所示,用公式可以描述:滿足P[i-s, i] == P[m-s, m]的最大長度s。
構建suffix數組的僞代碼如下:
- suffix[m-1]=m;
- for (i=m-2;i>=0;--i){
- q=i;
- while(q>=0&&P[q]==P[m-1-i+q])
- --q;
- suffix[i]=i-q;
- }
有了suffix數組,就可以定義bmGs[]數組,bmGs[i] 表示遇到好後綴時,模式串應該移動的距離,其中i表示好後綴前面一個字符的位置(也就是壞字符的位置),構建bmGs數組分爲三種情況,分別對應上述的移動模式串的三種情況
模式串中沒有子串匹配上好後綴,但找不到一個最大前綴
模式串中有子串匹配上好後綴
模式串中沒有子串匹配上好後綴,但找到一個最大前綴
構建bmGs數組的僞代碼如下:
- void preBmGs(char *x, int m, int bmGs[]) {
- int i, j, suff[XSIZE];
- suffixes(x, m, suff);
- //模式串中沒有子串匹配上好後綴,也找不到一個最大前綴
- for (i = 0; i < m; ++i)
- bmGs[i] = m;
- 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;
- //模式串中有子串匹配上好後綴
- for (i = 0; i <= m - 2; ++i)
- bmGs[m - 1 - suff[i]] = m - 1 - i;
- }
在計算完bmBc,BmGs數組後,BM算法僞代碼實現如下:
- j = 0;
- while (j <= strlen(T) - strlen(P)) {
- for (i = strlen(P) - 1; i >= 0 && P[i] ==T[i + j]; --i)
- if (i < 0)
- match;
- else
- j += max(bmGs[i], bmBc[T[i]]-(m-1-i));
- }
BM算法一般情況下的算法複雜度爲O(M/N),M爲字符串長度,N爲模式串長度。對於算法複雜度感興趣的,點這裏
參考文獻:
- Computer Algorithms: Boyer-Moore String Searching
- 字符串匹配那些事(一)
- 字符串匹配的Boyer-Moore算法
- A Fast String Searching Algorithm
JAVA實現源碼
- import java.util.*;
- public class BoyerMoore {
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- // String text="HERE IS A SIMPLE EXAMPLE";
- // String pattern="EXAMPLE";
- // String pattern="GCAGAGAG";
- // String text="WOWOWO!";
- // String pattern="WOWO";
- String text="中國是一個偉大的國度;偉大的祖國啊";
- String pattern="偉大的國度";
- BoyerMoore bm=new BoyerMoore();
- bm.boyerMoore(pattern, text);
- }
- private void preBmBc(String pattern,int patLength,Map<String,Integer> bmBc)
- {
- System.out.println("bmbc start process...");
- for(int i=patLength-2;i>=0;i--)
- {
- if(!bmBc.containsKey(String.valueOf(pattern.charAt(i))))
- {
- bmBc.put(String.valueOf(pattern.charAt(i)),(Integer)(patLength-i-1));
- }
- }
- }
- private void suffix(String pattern,int patLength,int [] suffix)
- {
- suffix[patLength-1]=patLength;
- int q=0;
- for(int i=patLength-2;i>=0;i--)
- {
- q=i;
- while(q>=0&&pattern.charAt(q)==pattern.charAt(patLength-1-i+q))
- {
- q--;
- }
- suffix[i]=i-q;
- }
- }
- private void preBmGs(String pattern,int patLength,int []bmGs)
- {
- int i,j;
- int []suffix=new int[patLength];
- suffix(pattern,patLength,suffix);
- //模式串中沒有子串匹配上好後綴,也找不到一個最大前綴
- for(i=0;i<patLength;i++)
- {
- bmGs[i]=patLength;
- }
- //模式串中沒有子串匹配上好後綴,但找到一個最大前綴
- j=0;
- for(i=patLength-1;i>=0;i--)
- {
- if(suffix[i]==i+1)
- {
- for(;j<patLength-1-i;j++)
- {
- if(bmGs[j]==patLength)
- {
- bmGs[j]=patLength-1-i;
- }
- }
- }
- }
- //模式串中有子串匹配上好後綴
- for(i=0;i<patLength-1;i++)
- {
- bmGs[patLength-1-suffix[i]]=patLength-1-i;
- }
- System.out.print("bmGs:");
- for(i=0;i<patLength;i++)
- {
- System.out.print(bmGs[i]+",");
- }
- System.out.println();
- }
- private int getBmBc(String c,Map<String,Integer> bmBc,int m)
- {
- //如果在規則中則返回相應的值,否則返回pattern的長度
- if(bmBc.containsKey(c))
- {
- return bmBc.get(c);
- }else
- {
- return m;
- }
- }
- public void boyerMoore(String pattern,String text )
- {
- int m=pattern.length();
- int n=text.length();
- Map<String,Integer> bmBc=new HashMap<String,Integer>();
- int[] bmGs=new int[m];
- //proprocessing
- preBmBc(pattern,m,bmBc);
- preBmGs(pattern,m,bmGs);
- //searching
- int j=0;
- int i=0;
- int count=0;
- while(j<=n-m)
- {
- for(i=m-1;i>=0&&pattern.charAt(i)==text.charAt(i+j);i--)
- { //用於計數
- count++;
- }
- if(i<0){
- System.out.println("one position is:"+j);
- j+=bmGs[0];
- }else{
- j+=Math.max(bmGs[i],getBmBc(String.valueOf(text.charAt(i+j)),bmBc,m)-m+1+i);
- }
- }
- System.out.println("count:"+count);
- }
- }