Boyer-Moore 算法详解

来源:互联网 发布:本子作者知乎 编辑:程序博客网 时间:2024/05/02 04:45

  写在前面:我的本意是想以通俗的语言来介绍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 BoyerJ Strother Moore1977年提出的一种字符串严格匹配(Exact String Matching)算法,它据说是常规应用中效率最高的[3],其时间复杂度可以达到亚线性,而且对于没有周期规律的模式串,最差情况下也只需要3n次比较。


约定和术语

约定

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

变量使用斜体

强调使用粗体

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

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

术语

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

mpattern的长度

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

ntext的长度

一、原理

1.1、概述

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

其思想简单表示如下:

计算bad-character跳转表

计算good-suffix跳转表

n  |text|

 |pattern|

j  0

While j  n - m:

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

若匹配成功:

报告一次成功匹配

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

否则:

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

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

bad-character跳转的长度

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

令 j  k

1.2Bad-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中的atext对齐,才有可能实现正确的匹配。

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

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

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

注意Bad-character跳转表中记录的只是每个字符最后出现的地方,因此不难观察发现,对于多次出现的字符,bad-character跳转表反而可能导致负的跳转为此,[2] 提出了一种“扩展的bad-character规则”记录pattern中每一个位置i上,字符c先于i出现最后位置,即对于 i < m,记录bcTable’[ic] = max{ jpattern[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.3Good-suffix跳转原理说明

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

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

Text

 

 

 

 

 

 

 

 

a

 

u

 

 

 

 

 

pattern




 

 

 

 

 

b

 

u

 

shift









 

 

c

 

u

 

 

 

 


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

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

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

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

text

 

 

 

 

 

 

 

 

a

 

u

 

 

 

 

 




pattern




 

 

 

 

 

b

 

u

 


shift













v

 

 

 

 

 

 

 

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

不妨定义R'(i)R'(ipatterni : 的能成为 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),则位置-2a注定不匹配;若右移两位(#2),则位置-4a注定不匹配;依此类推。

同样的道理,若仅有末位的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+11): # 情况二

            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') 可以验证这个过程。