字符串匹配的Boyer-Moore算法

公司內部培訓我想講一講grep命令的使用,正好網上有一篇文章說GNU grep命令內部字符串匹配算法用的是Boyer-Moore算法,此算法比KMP算法快3到5倍.好,那我們看看Boyer-Moore算法是如何匹配字符串的。

Boyer-Moore算法

在用於查找子字符串的算法當中,BM(Boyer-Moore)算法是目前被認爲最高效的字符串搜索算法,它由Bob Boyer和J Strother Moore設計於1977年。

一般情況下,比KMP算法快3-5倍。該算法常用於文本編輯器中的搜索匹配功能,比如大家所熟知的GNU grep命令使用的就是該算法,這也是GNU grep比BSD grep快的一個重要原因。

主要特徵

假設文本串text長度爲n,模式串pattern長度爲m,BM算法的主要特徵爲:

  • 從右往左進行比較匹配(一般的字符串搜索算法如KMP都是從從左往右進行匹配);
  • 算法分爲兩個階段:預處理階段和搜索階段;
  • 預處理階段時間和空間複雜度都是是O(m+),是字符集大小,一般爲256;
  • 搜索階段時間複雜度是O(mn);
  • 當模式串是非週期性的,在最壞的情況下算法需要進行3n次字符比較操作;
  • 算法在最好的情況下達到O(n/m),比如在文本串b中搜索模式串ab ,只需要n/m次比較。

算法基本思想

常規的匹配算法移動模式串的時候是從左到右,而進行比較的時候也是從左到右的,基本框架是:

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算法在移動模式串的時候是從左到右,而進行比較的時候是從右到左的,基本框架是:

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()儘可能大)。

一些說明

下面,就Moore教授自己的例子來解釋這種算法。

假定字符串text爲”HERE_IS_A_SIMPLE_EXAMPLE”,搜索詞pattern爲”EXAMPLE”。

HERE_IS_A_SIMPLE_EXAMPLE
EXAMPLE

首先,”字符串”與”搜索詞”頭部對齊,從尾部開始比較。

這是一個很聰明的想法,因爲如果尾部字符不匹配,那麼只要一次比較,就可以知道前7個字符(整體上)肯定不是要找的結果。

我們看到,”S”與”E”不匹配。這時,”S”就被稱爲”壞字符”(bad character),即不匹配的字符。

如果比較的字符串位置爲如下所示:

HERE_IS_A_SIMPLE_EXAMPLE
         EXAMPLE

pattern中的”MPLE”與text完全匹配,我們把這種情況稱爲”好後綴”(good suffix),即所有尾部匹配的字符串。
注意,”MPLE”、”PLE”、”LE”、”E”都是好後綴。

BM算法理論探討

壞字符算法

當出現一個壞字符時, BM算法向右移動模式串, 讓模式串中最靠右的對應字符與壞字符相對,然後繼續匹配。

壞字符算法有兩種情況。

Case1:模式串中有對應的壞字符時,讓模式串中最靠右的對應字符與壞字符相對(PS:BM不可能走回頭路,因爲若是回頭路,則移動距離就是負數了,肯定不是最大移動步數了),如下圖。

這裏寫圖片描述

Case2:模式串中不存在壞字符,很好,直接右移整個模式串長度這麼大步數,如下圖。
這裏寫圖片描述

好後綴算法

如果程序匹配了一個好後綴, 並且在模式中還有另外一個相同的後綴或後綴的部分, 那把下一個後綴或部分移動到當前後綴位置。

假如說,pattern的後u個字符和text都已經匹配了,但是接下來的一個字符不匹配,我需要移動才能匹配。

如果說後u個字符在pattern其他位置也出現過或部分出現,我們將pattern右移到前面的u個字符或部分和最後的u個字符或部分相同,

如果說後u個字符在pattern其他位置完全沒有出現,很好,直接右移整個pattern。

這樣,好後綴算法有三種情況,如下圖所示:

Case1:模式串中有子串和好後綴完全匹配,則將最靠右的那個子串移動到好後綴的位置繼續進行匹配。
這裏寫圖片描述

Case2:如果不存在和好後綴完全匹配的子串,則在好後綴中找到具有如下特徵的最長子串,使得P[m-s…m]=P[0…s]。(此圖其實非常清楚明白的說明了,如果是部分好後綴匹配的話,此匹配的好後綴一定是要在模式串的最前面,也就是頭部)
這裏寫圖片描述

Case3:如果完全不存在和好後綴匹配的子串,則右移整個模式串。

移動規則

BM算法的移動規則是:
將3中算法基本框架中的j += BM(),換成j += MAX(shift(好後綴),shift(壞字符)),
即BM算法是每次向右移動模式串的距離是,按照好後綴算法和壞字符算法計算得到的最大值。

shift(好後綴)和shift(壞字符)通過模式串的預處理數組的簡單計算得到。壞字符算法的預處理數組是bmBc[],好後綴算法的預處理數組是bmGs[]。

BM算法應用一

我們使用BM算法來應用在Moore教授自己的例子上,來解釋一下這種算法。
假定字符串
text爲”HERE_IS_A_SIMPLE_EXAMPLE”,搜索詞pattern爲”EXAMPLE”。

HERE_IS_A_SIMPLE_EXAMPLE
EXAMPLE

從pattern的最右邊開始與text比較,pattern[6]=”E”與“S”不相等時,“S”爲壞字符,並且“S”在pattern中沒有匹配的字符,則根據壞字符算法的第二種情況,直接整體將pattern整個移動7(strlen(pattern))位。

HERE_IS_A_SIMPLE_EXAMPLE
       EXAMPLE

從pattern的最右邊開始與text比較,pattern[6]=”E”與“P”不相等時,“P”爲壞字符,並且“P”在pattern中有匹配的字符pattern[4]=”P”,則根據壞字符算法的第一種情況,直接整體將pattern整個移動2位,以後pattern中最靠右的對應字符pattern[4]=”P”與壞字符相對。

HERE_IS_A_SIMPLE_EXAMPLE
         EXAMPLE

現在,這個情況我們可以看到pattern看的後四位與text是完全匹配的,也就是說“MPLE”是好後綴,pattern[2]=”A”與text中的“I”是不匹配的,也就是說“I”是壞字符。

根據壞字符算法,此情況屬於壞字符算法中的第二種情況,我們直接將pattern移動3位:

HERE_IS_A_SIMPLE_EXAMPLE
            EXAMPLE

我們再根據好後綴算法,此情況屬於好後綴算法中的第二種情況,其中pattern[0]=”E”與好後綴(“MPLE”,“PLE”,“LE”,“E”)中的”E”匹配,我們直接將pattern移動6位,使得pattern[0]=”E”與text中的“E”匹配:

HERE_IS_A_SIMPLE_EXAMPLE
               EXAMPLE

說明:

如果”好後綴”有多個,則除了最長的那個”好後綴”,其他”好後綴”的上一次出現位置必須在頭部。

比如,假定”BABCDAB”的”好後綴”是”DAB”、”AB”、”B”,請問這時”好後綴”的上一次出現位置是什麼?

回答是,此時採用的好後綴是”B”,它的上一次出現位置是頭部,即第0位。
這個規則也可以這樣表達:如果最長的那個”好後綴”只出現一次,則可以把搜索詞改寫成如下形式進行位置計算”(DA)BABCDAB”,即虛擬加入最前面的”DA”。

所以,此時,綜合壞字符算法(移動2位)與好後綴算法(移動6位),BM算法爲選擇其中最大的值,故我們選擇好後綴算法移動6位,如下圖:

HERE_IS_A_SIMPLE_EXAMPLE
               EXAMPLE

此時pattern[6]=”E”與“P”比較不匹配,則“P”爲壞字符,壞字符”P”與patter[4]=”P”匹配,根據壞字符算法的第二種情況,我們移動2位:

HERE_IS_A_SIMPLE_EXAMPLE
                 EXAMPLE

尾部開始逐位比較,發現全部匹配,於是搜索結束。如果還要繼續查找(即找出全部匹配),則根據”好後綴規則”,後移 6位,即頭部的”E”移到尾部的”E”的位置。

BM算法具體執行

BM算法子串比較失配時,按壞字符算法計算pattern需要右移的距離,要藉助bmBc數組,而按好後綴算法計算pattern右移的距離則要藉助bmGs數組。下面講下怎麼計算bmBc[]和bmGs[]這兩個預處理數組。

計算壞字符數組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。

這裏寫圖片描述

計算好後綴數組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;
    }
}

o easy? 結束了嗎?還差一步呢,這裏的suff[]咋求呢?

在計算bmGc數組時,爲提高效率,先計算輔助數組suff[]表示好後綴的長度。

suff數組的定義:m是pattern的長度

a. suffix[m-1] = m;
b. suffix[i] = k
for [ pattern[i-k+1] ….,pattern[i]] == [pattern[m-1-k+1],pattern[m-1]]
看上去有些晦澀難懂,實際上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;
    }
}

參考資料

1.字符串匹配的Boyer-Moore算法
http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html
2.grep之字符串搜索算法Boyer-Moore由淺入深(比KMP快3-5倍)
http://blog.jobbole.com/52830/
3.字符串搜索算法Boyer-Moore的Java實現
http://blog.csdn.net/nmgrd/article/details/51697567
4.Boyer-Moore算法學習
http://blog.csdn.net/sealyao/article/details/4568167
5.grep之字符串搜索算法Boyer-Moore由淺入深(比KMP快3-5倍)
http://www.cnblogs.com/lemon66/p/4858890.html

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