字符串匹配算法(二)


     我們在字符串匹配算法(一)學習了BF算法和RK算法,那有沒更加高效的字符串匹配算法呢。我們今天就來聊一聊BM算法。

BM算法

       我們把模式串和主串的匹配過程,可以看做是固定主串,然后模式串不斷在往后滑動的過程。當遇到不匹配的字符時,BF算和RK算法的做法是,把模式串向后滑動一位,然后從模式串的第一位開始重新匹配。如下圖所示。

      由於BF算法和RK算法,在遇到不匹配的字符時,模式串只是向后滑動一位,這樣的話時間復雜度比較高,那有沒有什么算法可以一下子多滑動幾位呢?比如遇到主串A中的字符d,由於d不在模式串中,所以只要d和模式串有重合,那就肯定不能匹配。所以我們可以直接多滑動幾位,直接滑到d的后面,然后再繼續匹配,這樣不就提高了效率了嗎?

      今天要聊的BM算法,本質上就是尋找這種規律。借助這種規律,在模式串和主串匹配的過程中,當模式串和主串遇到不匹配的字符時,能夠跳過一些肯定不匹配的情況,多往后滑動幾位。

BM算法的原理

      BM算法包含2部分,分別是壞字符規則和好后綴規則。

1.壞字符規則

     我們在BF算法和RK算法中,在模式串和主串匹配的過程中,我們都是按模式串的下標從小到大的順序依次匹配的。而BM算法的匹配順序則相反,是從大到小匹配的。如下所示。

     從模式串的末尾倒着匹配,當發現主串中某個字符匹配不上時,我們就把這個字符稱為壞字符。我們拿着壞字符d在模式串中查找,發現d不在模式串中。這個時候,我們可以將模式串直接滑動3位,滑動到字符d的后面,然后再從模式串的末尾開始比較。

        這個時候,我們發現主串中的字符串b和模式串的中的c不匹配。這個時候由於壞字符b在模式串中是存在的,模式串中下標為1的位置也是字符b,所以我們可以把模式串向后滑動1位,讓主串中的b和模式串中的b相對齊。然后再從模式串的末尾字符開始重新進行匹配。

       從上面的例子中,我們可以總結出規律。當發生不匹配的時候,我們把壞字符對應的模式串中的字符下標記做Ai。如果壞字符在模式串中存在,我們把這個壞字符在模式串中的下標記做Bi(如果壞字符在模式串中出現多次,我們把靠后的那個位置記做是Bi,這么做是為了不讓模式串向后滑動過多,導致可能匹配的情況錯過)。那模式串向后滑動的位數就是Ai-Bi。

       不過單純的使用壞字符規則是不夠的。因為根據Ai-Bi計算出來的移動位數有可能是負數。比如主串是aaaaaa,模式串是baaa。所以,BM算法還需要用到“好后綴規則”。

2.好后綴規則

        好后綴規則和壞字符規則思路上很相似。如下圖所示。

     當模式串滑動到圖中的位置時,模式串和主串有2個字符是匹配的,倒數第三個字符發生了不匹配的情況。我們把已經匹配的ab叫做好后綴,記做{u}。我們拿它在模式串中進行尋找另一個和{u}相匹配的子串{u*}。那我們就將模式串滑動到子串{u*}和主串{u}對齊的位置。

     如果在模式串中找不到另一個等於{u}的子串,我們就直接將模式串,滑動到主串{u}的后面。因為之前的任何一次往后滑動,都沒有匹配主串{u}的情況。不過,當模式串中不存在等於{u}的子串時,我們直接將模式串滑動到{u}的后面,這樣是否會錯過可能匹配的情況呢。如下所示,這里的ab是好后綴,盡管在模式串中沒有另一個相匹配的子串{u*},但如果我們將模式串移動到{u}的后面,那就錯過了模式串和主串相匹配的情況。

     所以,當模式串滑動到前綴與主串中{u}的后綴有部分重合的時候,並且重合的部分相等的時候,就有可能會存在完全匹配的情況。針對這種情況,我們不僅要看好后綴在模式串中,是否存在另一個匹配的子串。我們還要考察好后綴的后綴子串,是否和模式串的前綴子串相匹配。

      這里我們再來解釋一下字符串的后綴子串和前綴子串。所謂字符串A的后綴子串,就是最后一個字符跟A對齊的子串,比如,字符串abc的后綴子串是c、bc。所謂的前綴子串,就是起始字符和A對齊的子串。比如,字符串abc的前綴子串是a、ab。我們從好后綴子串中,找一個最長並且能和模式串前綴子串匹配的,假如是{v}。然后滑動到如圖所示的位置。

       到目前位置,我們的壞字符和好后綴就講完了,我們接下來想這么一個問題。當模式串和主串中某個字符不匹配的時候,我們是選好后綴規則呢還是壞字符規則來計算向后滑動的位數呢?

       我們可以分別計算壞字符規則和好后綴規則向后滑動的位數,然后取兩個數的最大的,作為模式串往后滑動的位數。

BM算法的代碼實現

         我們接下來來看BM算法的代碼是如何實現的。

         "壞字符規則"中當遇到壞字符時,我們要計算后移的位數Ai-Bi,其中Bi的計算的重點。如果我們拿壞字符在模式串中順序查找,這樣是可以實現,不過效率比較低下。我們可以用大小256的數組,來記錄每個字符在模式串中出現的位置。數組的下標對應的是字符的Ascii編碼,數組中存儲的是這個字符在模式串中的位置。

SIZE = 256
def generateBC(b, m):
    bc=[-1]*SIZE
    for i in range(m):
        accii=ord(b[i])
        bc[accii]=i
    return b

        我們先把BM算法中的“壞字符規則”寫好,先不考慮“好后綴規則”,並且忽略掉Ai-Bi為負數的情況。

def bm(a,n,b,m):
     #生成bc散列表
     bc=generateBC(b,m)
     #i表示主串與模式串對齊的第一個字符
     i = 0
     while i<=n-m:
          for j in range(m-1,-2,-1): #模式串從后往前匹配
               if(a[i+j]!=b[j]): #壞字符對應模式串中的下標是j
                    break
​
          if(j<0):
               #表示匹配成功
               return i
          #Ai-Bi,等同於向后滑動幾位
          i = i + (j - bc[ord(a[i+j])])
     return -1

  到目前為止,我們已經實現了“壞字符規則”的代碼框架,剩下就是需要往里面添加“好后綴規則”的代碼邏輯了。在繼續講解之前,我們先簡單回顧一下,前面講的“好后綴規則”中最關鍵的內容。

  • 在模式串中,查找和好后綴匹配的另一個子串。

  • 在好后綴的后綴子串中,查找最長的,能跟模式串前綴子串相匹配的后綴子串。

      我們可以這么考慮,因為好后綴也是模式串的后綴子串,所以,我們可以在模式串和主串進行匹配之前,通過預處理模式串,預先計算好模式串中的每個后綴子串,對應的另外一個可匹配子串中的位置。這個預處理過程有點復雜。大家可以多讀幾遍,在紙上畫一畫。

     我們先來看如何表示模式串中的不同后綴子串呢?因為后綴子串的最后一個字符的位置是固定的,下標為m-1,所以我們只需要記錄長度就可以了。通過長度,我們可以唯一確定一個后綴子串。

      下面我們引入一個關鍵的數組suffix。suffix數組的下標k,表示后綴子串的長度,數組對應的位置存儲的是,在模式串中和好后綴{u}相匹配的子串{u*}的起始下標位置。

      其中有一個點需要注意,如果模式串中有多個子串跟后綴子串相匹配,我們選最靠后的那個子串的起始位置,以免滑動的太遠,錯過可能匹配的情況。

    接下來我們來看好后綴規則的第二條,就是要在好后綴的后綴子串中,查找最長的能跟模式串前綴子串匹配的后綴子串。接來下,我們來引入另一個數組變量prefix,來記錄模式串的后綴子串是否能匹配模式串的前綴子串。

 

    接下來我們來看如何給這兩個數組賦值呢?我們拿下標為0~i的子串(i 可以是 0 到 m-2)與整個模式串,求公共后綴子串。如果公共后綴子串的長度是 k,那我們就記錄 suffix[k]=j(j 表示公共后綴子串的起始下標)。如果 j 等於 0,也就是說,公共后綴子串也是模式串的前綴子串,我們就記錄 prefix[k]=true。我們來看代碼實現。

def generateSP(b,m):
     suffix= [-1]*m
     prefix= [False]*m
     for i in range(m-1):
          j=i
          k=0 #公共后綴子串長度
          while (j>=0 and b[j]==b[m-1-k]):
               j=j-1
               k=k+1
               #j+1表示公共后綴子串在b[0, i]中的起始下標
               suffix[k]=j+1
​
          if (j==-1):
               #如果公共后綴子串也是模式串的前綴子串
               prefix[k] = True
​
     return suffix,prefix

 下面我們來看如何根據好后綴規則,來計算模式串往后滑動的位數?假設好后綴的長度是 k。我們先拿好后綴,在 suffix 數組中查找其匹配的子串。如果 suffix[k]不等於 -1(-1 表示不存在匹配的子串),那我們就將模式串往后移動 j-suffix[k]+1 位(j 表示壞字符對應的模式串中的字符下標)。如果 suffix[k]等於 -1,表示模式串中不存在另一個跟好后綴匹配的子串片段。我們就用下面這條規則來處理。好后綴的后綴子串 b[r, m-1](其中,r 取值從 j+2 到 m-1)的長度 k=m-r,如果 prefix[k]等於 true,表示長度為 k 的后綴子串,有可匹配的前綴子串,這樣我們可以把模式串后移 r 位。如果兩條規則都沒有找到可以匹配的好后綴及其后綴子串的后綴子串,我們就將整個模式串后移 m 位。到此為止,我們的好后綴規則也聊完了,我們現在把好后綴規則的代碼插入到前面的框架中,就可以得到完整版本的BM算法了。

#散列表的大小
SIZE = 256
def generateBC(b, m):
    bc=[-1]*SIZE
    for i in range(m):
        accii=ord(b[i])
        bc[accii]=i
​
    return bc
​
def generateSP(b,m):
     suffix= [-1]*m
     prefix= [False]*m
     for i in range(m-1):
          j=i
          k=0 #公共后綴子串長度
          while (j>=0 and b[j]==b[m-1-k]):
               j=j-1
               k=k+1
               #j+1表示公共后綴子串在b[0, i]中的起始下標
               suffix[k]=j+1
​
          if (j==-1):
               #如果公共后綴子串也是模式串的前綴子串
               prefix[k] = True
​
     return suffix,prefix
​
#j表示壞字符對應的模式串中的字符下標
#m表示模式串長度
def moveSP(j,m,suffix,prefix):
     #好后綴的長度
     k=m-1-j
     if suffix[k]!=-1:
          return j-suffix[k]+1
     for r in range(j+2,m):
          if prefix[m-r]==True:
               return r
     return m
​
def bm(a,n,b,m):
     #生成bc散列表
     bc=generateBC(b,m)
     suffix, prefix = generateSP(b,m)
     #i表示主串與模式串對齊的第一個字符
     i = 0
     while i<=n-m:
          for j in range(m-1,-2,-1): #模式串從后往前匹配
               if(a[i+j]!=b[j]): #壞字符對應模式串中的下標是j
                    break
​
          if(j<0):
               #表示匹配成功
               return i
          #Ai-Bi,等同於先后滑動幾位
          x = j - bc[ord(a[i+j])]
          y = 0
          #如果有好后綴的話
          if j < m-1:
             y = moveSP(j, m, suffix, prefix)
​
          i = i + max(x, y)
     return -1
   到此為止,我們BM算法就聊完。更多硬核知識,請關注公眾號。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM