字符串匹配算法(二)-BM算法详解

时间:2024-01-29 07:21:58

     我们在字符串匹配算法(一)学习了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算法就聊完。更多硬核知识,请关注公众号。