写在前面:我的本意是想以通俗的语言来介绍Boyer-Moore,可是“学术”一点的语言毕竟有它存在的合理性,所以,额……
我的另一个意图是想详细介绍good-suffix shift的计算方法——因为中文世界中看不到对其完整的介绍:涉及到good-suffix的地方不是被直接跳过,就是仅仅给出一段代码——而这个部分的算法其实相对于Boyer-Moore本身来说更是精巧。
可惜对 Good-suffix 计算的介绍写出来之后因为格式比较复杂,又正赶上优快云封图自宫,不易发表在博客上,故导出为PDF,感兴趣者可以前往查看:
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 |
m ← | 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 ← 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 出现最后位置,即对于0 ≤ 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 : m ] 的能成为 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 ) m = 297次比较操作,这个差距应该说是非常大的。
使用后面实现的 Python 代码( http://docs.google.com/leaf?id=0B9sqyhyu5n-UM2ZmMzYxMTYtZDY5YS00ZTE5LWIxMTYtYTcyMmJiY2M3ODQ1&hl=zh_CN ),通过调用 bm ('agcatagcatacaagagaagagacagtagagactatta', 'agagacagtag') 可以验证这个过程。