Boyer-Moore算法

一.簡述

        在當前用於查找子字符串的算法中,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爲模式串長度。對於算法複雜度感興趣的,點這裏

參考文獻:


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);
	}

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章