Boyer-Moore 算法详解

  写在前面:我的本意是想以通俗的语言来介绍Boyer-Moore,可是“学术”一点的语言毕竟有它存在的合理性,所以,额……

  我的另一个意图是想详细介绍good-suffix shift的计算方法——因为中文世界中看不到对其完整的介绍:涉及到good-suffix的地方不是被直接跳过,就是仅仅给出一段代码——而这个部分的算法其实相对于Boyer-Moore本身来说更是精巧。

  可惜对 Good-suffix 计算的介绍写出来之后因为格式比较复杂,又正赶上CSDN封图自宫,不易发表在博客上,故导出为PDF,感兴趣者可以前往查看:

http://docs.google.com/leaf?id=0B9sqyhyu5n-UM2ZmMzYxMTYtZDY5YS00ZTE5LWIxMTYtYTcyMmJiY2M3ODQ1&hl=zh_CN

 

  BTW. Boyer-Moore这算法对我而言算是复杂的了,可是在 grep 的 src/kwset.c 中我看到了这样的东西:/* Build the Boyer Moore delta.  Boy that's easy compared to CW. */ ,很是郁闷,下一步就看看CW感受一下去。

 

 


下面正式开始——

 

 

Boyer-Moore算法是 Bob Boyer J Strother Moore 1977 年提出的一种字符串严格匹配( Exact String Matching )算法,它据说是常规应用中效率最高的 [3] ,其时间复杂度可以达到亚线性,而且对于没有周期规律的模式串,最差情况下也只需要3 n 次比较。


约定和术语

约定

字符串和数组的下标均以0 为起始,下标为负代表倒数

变量使用 斜体

强调使用 粗体

在区间的表示中, S a  :  b  ] 代表在 S 处于区间  a b  ) 的部分

在区间的表示中, S a  ..  b  ] 代表 S 处于区间  a b  ] 的部分

术语

pattern :模式串,即要查找的目标

m pattern 的长度

text :文本串,字符串匹配算法将在 text 中查找 pattern 出现的位置

n text 的长度

一、原理

1. 1、概述

Boyer-Moore算法从右向左扫描模式串中的字符;当遇到不匹配的字符时,它通过预先计算的 Bad-character 跳转表以及 Good-suffix 跳转表寻找最大的跳转长度

其思想简单表示如下:

计算bad-character 跳转表

计算good-suffix 跳转表

n     | text |

  | pattern |

j    0

While  j     n  -  m :

从右向左匹配 pattern  text 的子串 text j   :   j + m  ]

若匹配成功:

报告一次成功匹配

令  j     j  +  good-suffix-table[0]

否则:

根据匹配失败的位置 i 得到good-suffix 跳转长度;

根据匹配失败的位置 i 和导致匹配失败的字符 c 得到

bad-character跳转的长度

比较上面两个长度,选择较大的一个,令其为 k

令  j     k

1. 2 B ad-character跳转原理说明

当匹配过程中遇到了不匹配的字符时,可以移动窗口使文本串中不匹配的字符a 与模式串中字符 a 最后出现的位置对齐。

考虑如下情况:

text

 

 

 

 

 

 

 

 

a

 

u

 

 

 

 

 

 

pattern





 


 

 

b

 

u

 






1.1  不匹配的情况

将模式串中的a 与文本串中的对齐,我们得到:

text

 

 

 

 

 

 

 

 

a

 

u

 

 

 

 

 

 

pattern








 

a

 

a

 



1.2  发生不匹配,且pattern 中含有 a

而若a 在模式串中不存在,我们则可以将窗口移动到 a 出现的位置之后:

Text

 

 

 

 

 

 

 

 

a

 

u

 

 

 

 

 

 

pattern










 

 

a

 

 

1.3  发生不匹配,且pattern 中不含有 a

这样做的意义很明显:

text

 

 

 

 

 

 

 

 

a

 

u

 

 

 

 

 

 




pattern





 

a

 

a

 








1







 

a

 

a

 







2








 

a

 

a

 






3









 

a

 

a

 





4

1.4  常规的窗口移动方法

如图1.4 所示,若第一次匹配在遇到字符 a 的时候失败了,那么按照常规的、顺序的窗口移动方法,第2 次和第 3 次尝试也 不可能 得到正确的匹配,而只有将 pattern 中的 a text 对齐,才 有可能 实现正确的匹配。

同样的道理,若整个 pattern 中不含有 a ,则可以安全的将窗口移动到 a 出现的位置之后。

通过将 每个字符最后出现的位置 记录在表 bcTable 中(没有出现的字符则令其处于-1 位置),可以方便的将不匹配字符与模式串中该字符出现的最后位置对齐;因此定义 bcTable 为:

对于字符集中的每一个字符c bcTable [c]  = m ax { i   :   0  ≤  i  <   m     pattern [ i ]=c} c 存在于 pattern 中,其他情况为 -1

注意 B ad-character跳转表中记录的只是每个字符最后出现的地方,因此不难观察发现,对于多次出现的字符, bad-character 跳转表反而可能导致负的跳转 为此, [2]   提出了一种“扩展的bad-character 规则” 记录 pattern 中每一个位置 i 上,字符 c 先于 i 出现最后位置,即对于   i  <  m ,记录 bcTable’ [ i c ] = max{  j pattern [ j ] =  c  且  j  <  i  } 。对于小的字符集而言,这会对效率起到很大程度的改进,但由于很多实际情况下这个做法反而会导致性能的损失,因此较少采用。

为了更加便于理解,这里给出根据定义求bad-character 跳跃表的 Python 代码:

def   bmbc  ( p, charset=r ' agct '  ):

    bc = {}

    lp = len(p)

     for   c   in  charset:

        bc[ c ] = - 1

     for  i  in  range(lp- 1 ):

        bc[ p[i] ] = i 

     return  bc

其中,参数p 为模式串,参数 charset 为字符集(默认为 DNA 碱基序列 agct );返回值为字符集对应的坏字符跳转表。

1. 3 G ood-suffix跳转原理说明

假设通过自右向左的匹配,已经得到了 pattern i   :   m  ] =  text j + i   :   j + m  ] =  u ,且 pattern i -1 ]  ≠  text j + i -1 ] ,那么可以分两种情况:

情况一 pattern 中,在 i 之前还存在子串 u ,并且子串 u 之前的字符不等于 pattern i -1 ]

Text

 

 

 

 

 

 

 

 

a

 

u

 

 

 

 

 

pattern




 

 

 

 

 

b

 

u

 

shift









 

 

c

 

u

 

 

 

 


1.5    u在匹配失败的地方之前重现,并且 u 前面的字符不为 b

不妨定义R( i ) R( i 是能使 pattern i  :  m  ] 成为 pattern  [ 0 : R( i ) ] 的后缀、且这样一个后缀前的字符不为 pattern i -1 ] 的最大值;若这样的值不存在,则令 R( i 为  -1

在图1.5 表示的这种情况下,我们可以移动窗口使 R( i 对齐模式串尾部当前所在的地方。

情况二 pattern 中, i 之前不存在子串 u ,但 pattern 的一个前缀 v u 的一个后缀相匹配:

text

 

 

 

 

 

 

 

 

a

 

u

 

 

 

 

 




pattern




 

 

 

 

 

b

 

u

 


shift













v

 

 

 

 

 

 

 

1.6     u的后缀 v 在字符串中重现

不妨定义 R'( i ) R'( i pattern i  :  的能成为  pattern  一个前缀的后缀(图示 v )的最大长度,若这样的值不存在,则为 -1

1.6 这种情况下,我们同样可以通过 移动窗口使R'( i 对齐模式串尾部当前所在的地方。

定义这样的good-suffix 跳转规则同样是为了避免不必要的比较操作,以模式串 gcagagag 为例,若末位的 g 成功匹配了,而倒数第二位的 a 没有匹配上,那么可以将窗口右移 7 位:

g

c

a

g

a

g

a

g

 

 

 

 

 

 

 

 

 


g

c

a

g

a

g

a

g








#1



g

c

a

g

a

g

a

g







#2




g

c

a

g

a

g

a

g






#3





g

c

a

g

a

g

a

g





#4






g

c

a

g

a

g

a

g




#5







g

c

a

g

a

g

a

g



#6

 

 

 

 

 

 

 

g

c

a

g

a

g

a

g

 

#7

1.7  仅末位匹配时的good-suffix 跳转情况示意

因为,如图1.7 所示,若右移一位(情况 #1 ),则位置 -2 a 注定不匹配;若右移两位( #2 ),则位置 -4 a 注定不匹配;依此类推。

同样的道理,若仅有末位的ag 得到匹配,那么可以安全的将当前窗口右移 4 位:

g

c

a

g

a

g

a

g

 

 

 

 

 

 

 

 

 


g

c

a

g

a

g

a

g








#1



g

c

a

g

a

g

a

g







#2




g

c

a

g

a

g

a

g






#3

 

 

 

 

g

c

a

g

a

g

a

g

 

 

 

 

#4






g

c

a

g

a

g

a

g




#5







g

c

a

g

a

g

a

g



#6






 

 

g

c

a

g

a

g

a

g


#7

1.8  末两位匹配的good-suffix 跳转情况示意

这里,#4 #7 都是可能得到正确匹配的情况,因此选择相对较小的跳转,以避免漏过匹配。

为了更便于理解,这里给出根据定义求good-suffix 跳跃表的 Python 代码:

def   bmgs  ( p ):

    lp = len(p)

    gs = [lp] * lp

    j  = lp

     while  j> 0 :

        ls = lp - j

         for  i  in  range(-ls+ 1 1 ):  # 情况二

             if  p[ 0 :ls+i] == p[j-i:lp]:

                gs[j- 1 ] = j-i

         for  i  in  range( 1 ,j):       # 情况一

             if  p[i:i+ls] == p[j:lp]  and  p[i- 1 ] != p[j- 1 ] :

                gs[j- 1 ] = j-i

        j = j- 1

     return  gs

其参数为模式串pattern ,返回值为对应的 good-suffix 跳转表。

1.4、完整的 Boyer-Moore 查找示例

以在字符串agcatagcatacaagagaagagacagtagagactatta 中查找 agagacagtag 为例,

Bad-character跳转表为: {'a': 9, 'c': 5, 't': 8, 'g': 7}

Good-suffix跳转表为: [9, 9, 9, 9, 9, 9, 9, 9, 3, 11, 1]

查找过程如下图:

a

g

c

a

t

a

g

c

a

t

a

c

a

a

g

a

g

a

a

g

a

g

a

c

a

g

t

a

g

a

g

a

c

t

a

t

t

a

a

g

a

g

a

c

a

g

t

a

g





























a

g

a

g

a

c

a

g

t

a

g

































a

g

a

g

a

c

a

g

t

a

g































a

g

a

g

a

c

a

g

t

a

g































a

g

a

g

a

c

a

g

t

a

g





























a

g

a

g

a

c

a

g

t

a

g

































a

g

a

g

a

c

a

g

t

a

g





































a

g

a

g

a

c

a

g

t

a

g

1.9  Boyer-Moore算法运行过程示例

从图1.9 中可以看出,在查找过程中, Boyer-Moore 算法做了 8 次尝试,总共 22 次比较操作;而常规的字串查找算法则会需要  ( n  -  m )   = 297次比较操作,这个差距应该说是非常大的。

使用后面实现的 Python 代码( http://docs.google.com/leaf?id=0B9sqyhyu5n-UM2ZmMzYxMTYtZDY5YS00ZTE5LWIxMTYtYTcyMmJiY2M3ODQ1&hl=zh_CN ),通过调用 bm ('agcatagcatacaagagaagagacagtagagactatta', 'agagacagtag')  可以验证这个过程。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值