字符串匹配算法 之 BM(Boyer-Moore)

背景

各種文本編輯器的”查找”功能(Ctrl+F),大多采用Boyer-Moore算法。

這裏寫圖片描述


大家所熟知的GNU grep命令使用的就是該算法,這也是GNU grep比BSD grep快的一個重要原因。
這裏寫圖片描述

1977年,德克薩斯大學的Robert S. Boyer教授和J Strother Moore教授發明了這種效率高,構思巧妙,容易理解的字符串匹配算法。

算法特徵

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

  • 從右往左進行比較匹配(一般的字符串搜索算法如KMP都是從從左往右進行匹配);

  • 算法分爲兩個階段:預處理階段和搜索階段;

  • 預處理階段時間和空間複雜度都是是O(m+sigma),sigma是字符集大小,一般爲256;

  • 搜索階段時間複雜度是O(mn);

  • 當模式串是非週期性的,在最壞的情況下算法需要進行3n次字符比較操作;

  • 算法在最好的情況下達到O(n / m),比如在文本串bn中搜索模式串am-1b ,只需要n/m次比較。

這些特徵先讓大家對該算法有個基本的瞭解,等看懂了算法再來看這些特徵又會有些額外的收穫。

算法基本思想

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

j = 0while(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 = 0while(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,因此你不管右移1、2、3、4位肯定還是不匹配,何必花這個功夫呢?直接右移5(strlen(pattern))位再進行比對不是更好嗎?

好,就這樣做,右移5位後,text中的b與pattern中的c比較,發現還是不同,這時咋辦?pattern中有b這個字母,所以pattern不能一下右移5位了,難道直接右移一位嗎?

No,可以直接將pattern中的b右移到text中b的位置進行比對,但是pattern中有兩個b,右移哪個b呢?

保險的辦法是用最右邊的b與text進行比對,爲啥?下圖說的很清楚了,用最左邊的b太激進了,容易漏掉真正的匹配,圖中用最右邊的b後發現正好所有的都匹配成功了。

如果用最左邊的不就錯過了這個匹配項嗎?這個啓發式搜索就是BM算法做的。

這裏寫圖片描述

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簡單的右移一步即可,堅持不走回頭路!

這裏寫圖片描述

好了,這就是所謂的“壞字符算法”,簡單吧,通俗易懂吧,上面用紅色粗體字標註出來的b就是“壞字符”,即不匹配的字符,壞字符是針對text的,也就是說壞字符是指出現在text中和pattern不匹配的字符。

BM難道就這麼簡單?就一個啓發式規則就搞定了?當然不是了,大家再次頭腦風暴一下,有沒有其他加快字符串搜索的方法呢?比如下面的例子

這裏寫圖片描述

一開始利用了壞字符算法一下移了4位,不錯,接下來遇到了回頭路,沒辦法只能保守移一位,但真的就只能移一位嗎?No,因爲pattern前面其他位置也有連續的片段和剛剛匹配成功的後綴ab一模一樣,那麼將pattern前面的ab右移到text剛匹配成功的ab對齊,再繼續往前匹配不是更好嗎?這樣就可以一次性右移兩位了,很好的有一個啓發式搜索規則啊。有人可能想:要是pattern前面沒有和已經匹配成功的後綴一樣的片段咋辦?是不是就無效了?不完全是,這要看情況了,比如下面這個例子。

這裏寫圖片描述

cbab這個後綴已經成功匹配,然後b沒成功,而pattern前面也沒發現cbab這樣的串,這樣就直接保守移一位?No,前面有ab啊,這是cbab後綴的一部分,也可以好好利用,直接將pattern前面的ab右移到text已經匹配成功的ab位置處繼續往前匹配,這樣一下子就右移了四位,很好。當然,如果前面完全沒已經匹配成功的後綴或部分後綴,比如最前面的babac,那就真的不能利用了。

好了,這就是所謂的“好後綴算法”,簡單吧,通俗易懂吧,上面用紅色字標註出來的ab(前面例子)和cbab(上面例子)就是“好後綴”,好後綴是針對pattern的

下面,最後再舉個例子說明啥是壞字符,啥是好後綴。

這裏寫圖片描述

BM就這麼簡單?是的,容易理解但並不是每個人都能想到的兩個啓發式搜索規則就造就了BM這樣一個優秀的算法。那麼又有個問題?這兩個算法怎麼運用,一下壞字符的,一下好後綴的,什麼時候該用壞字符?什麼時候該用好後綴呢?很好的問題,這就要看哪個右移的位數多了,比如上面的例子,一開始如果用好後綴的話只能移一位而用壞字符就能右移三位,此時當然選擇壞字符算法了。接下來如果繼續用壞字符則只能右移一位而用好後綴就能一下右移四位,這時候你說用啥呢?So,這兩個算法是“並行”的,哪個大用哪個。

光用例子說明當然不夠,太淺了,而且還不一定能完全覆蓋所有情況,不精確。下面就開始真正的理論探討了。

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算法的移動規則是:

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

BM算法 ——Big Move ,最大位移算法,這個是我開玩笑的,方便記憶。

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

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)。

寫成代碼也非常簡單:

def PreBmBc(pattern,m):
    bmBc = []
    for i in range(256):# ascii碼
        bmBc.append(m)
    for j in range(m):
        bmBc[ord(pattern[j])] = m - 1 - j#這裏把字符直接轉換成ascii碼,就是做一個映射,pattern
                                         #裏的每個字符映射到字符距離字符尾部的距離
    return bmBc

計算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

這裏寫圖片描述

這樣就清晰了,代碼編寫也比較簡單:

def PreBmGs(pattern, m):
    bmGs = []
    suff = suffix(pattern, m)
    # 全部賦值爲m,包含Case3
    for i in range(m):
        bmGs.append(m)
    # Case2
    for i in reversed(range(m)):
        if suff[i] == i + 1:
            for j in range(0,m-1-i):
                if bmGs[j] == m:
                    bmGs[j] = m - i - j
    # Case1
    for i in range(m-1):
        bmGs[m - 1 - suff[i]] = m - i -i
    return bmGs

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

這樣看來代碼也很好寫:

def suffix(pattern, m):
    suff = []
    suff[m-1] = m
    for i in reversed(range(m-1)):
        j = i
        while (j >= 0) and (pattern[m - 1 - i + j]):
            j -= 1
        suff[i] = i - j
    return suff

這樣可能就萬事大吉了,可是總有人對這個算法不滿意,感覺太暴力了,於是有聰明人想出一種方法,對上述常規方法進行改進。基本的掃描都是從右向左,改進的地方就是利用了已經計算得到的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可能會在這次得到匹配。

好了,這樣解釋過後,代碼也比較簡單:

def suffix(pattern, m):
    i = 0
    f = 0
    g = 0
    suff = []
    for i in range(256):
        suff.append(m)
    g = m - 1
    for i in reversed(range(m-1)):
        if (i>g) and (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) and (pattern[g] == pattern[g + m - 1 -f]):
                g -= 1
            suff[i] = f -g
    return suff

可以說重要的算法都完成了,希望大家能夠看懂,爲了驗證大家到底有沒有完全看明白,下面出個簡單的例子,大家算一下bmBc[]、suff[]和bmGs[]吧。

舉例如下:
這裏寫圖片描述

PS:這裏也許有人會問:bmBc[‘b’]怎麼等於2,它不是最後出現在pattern最後一個位置嗎?按定義應該是0啊。請大家仔細看下bmBc的算法:

for i in range(m):
    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。

好了,所有的終於都講完了,下面整合一下這些算法吧。

# -*- coding: utf-8 -*-
"""
Created on Thu Jul 28 23:11:27 2016

@author: zang
"""


def PreBmBc(pattern,m):
    bmBc = []
    for i in range(256):# ascii碼
        bmBc.append(m)
    for j in range(m):
        bmBc[ord(pattern[j])] = m - 1 - j#這裏把字符直接轉換成ascii碼,就是做一個映射,pattern裏的每個字符映射到字符距離字符尾部的距離
    return bmBc

def PreBmGs(pattern, m):
    bmGs = []
    suff = suffix(pattern, m)
    # 全部賦值爲m,包含Case3
    for i in range(m):
        bmGs.append(m)
    # Case2
    for i in reversed(range(m)):
        if suff[i] == i + 1:
            for j in range(0,m-1-i):
                if bmGs[j] == m:
                    bmGs[j] = m - i - j
    # Case1
    for i in range(m-1):
        bmGs[m - 1 - suff[i]] = m - i -i
    return bmGs

def suffix(pattern, m):
    i = 0
    f = 0
    g = 0
    suff = []
    for i in range(256):
        suff.append(m)
    g = m - 1
    for i in reversed(range(m-1)):
        if (i>g) and (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) and (pattern[g] == pattern[g + m - 1 -f]):
                g -= 1
            suff[i] = f -g
    return suff

def BoyerMoore(pattern, m, text, n):
    bmBc = PreBmBc(pattern, m)
    bmGs = PreBmGs(pattern, m)
    j = 0
    flag = 0
    results = []
    while j <= n - m:
        for i in reversed(range(m)):

            if pattern[i] == text[i + j]:
                if i <= 0:
                    #print "Find it, the position is ",j
                    flag += 1
                    results.append(" "*j + pattern + " "*(n - m - j) + "  " + str(j) + "  " + str(flag))
                    j += bmGs[0]
                    #return
            else:
                j += max(bmBc[ord(text[i + j])] - m + 1 + i, bmGs[i])
                break
    if flag == 0:
        print "No match."
    else:
        print flag," matching results are listed below."
        print "-------" + "-"*n + "-------"
        print text
        for line in results:
            print line
        print "-------" + "-"*n + "-------"

def main():
    while 1:
        text = raw_input("text: ")
        pattern = raw_input("pattern: ")
        if len(text) == 0 or len(pattern) == 0:
            print "\nplease input text and pattern again!"
            break
        BoyerMoore(pattern, len(pattern), text, len(text))

if __name__ == '__main__':
    main()

這裏寫圖片描述



這裏寫圖片描述



這裏寫圖片描述
漢字匹配的位置是按照字節算的。

參考

-http://www.cnblogs.com/lanxuezaipiao/p/3452579.html

發佈了126 篇原創文章 · 獲贊 94 · 訪問量 46萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章