Boyer-Moore 算法详解
2010-01-12 11:15
169 查看
写在前面:我的本意是想以通俗的语言来介绍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.
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.
当匹配过程中遇到了不匹配的字符时,可以移动窗口使文本串中不匹配的字符a
与模式串中字符
a
最后出现的位置对齐。
考虑如下情况:
图1.1
不匹配的情况
将模式串中的a
与文本串中的对齐,我们得到:
图1.2
发生不匹配,且pattern
中含有
a
而若a
在模式串中不存在,我们则可以将窗口移动到
a
出现的位置之后:
图1.3
发生不匹配,且pattern
中不含有
a
这样做的意义很明显:
图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.
假设通过自右向左的匹配,已经得到了
pattern
[
i
:
m
] =
text
[
j
+
i
:
j
+
m
] =
u
,且
pattern
[
i
-1 ]
≠
text
[
j
+
i
-1 ]
,那么可以分两种情况:
情况一
,
pattern
中,在
i
之前还存在子串
u
,并且子串
u
之前的字符不等于
pattern
[
i
-1 ]:
图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
的一个后缀相匹配:
图1.6
u的后缀
v
在字符串中重现
不妨定义
R'(
i
):
R'(
i
) 是
pattern
[
i
:
m
] 的能成为
pattern
一个前缀的后缀(图示
v
)的最大长度,若这样的值不存在,则为 -1
。
图1.6
这种情况下,我们同样可以通过
移动窗口使R'(
i
) 对齐模式串尾部当前所在的地方。
定义这样的good-suffix
跳转规则同样是为了避免不必要的比较操作,以模式串
gcagagag
为例,若末位的
g
成功匹配了,而倒数第二位的
a
没有匹配上,那么可以将窗口右移
7
位:
图1.7
仅末位匹配时的good-suffix
跳转情况示意
因为,如图1.7
所示,若右移一位(情况
#1
),则位置
-2
的
a
注定不匹配;若右移两位(
#2
),则位置
-4
的
a
注定不匹配;依此类推。
同样的道理,若仅有末位的ag
得到匹配,那么可以安全的将当前窗口右移
4
位:
图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、完整的
以在字符串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]
查找过程如下图:
图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')
可以验证这个过程。
我的另一个意图是想详细介绍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
|
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 | |
不匹配的情况
将模式串中的a
与文本串中的对齐,我们得到:
text | | | | | | | | | a | | u | | | | | | |
pattern | | a | | 不 | 包 | 含 | a | |
发生不匹配,且pattern
中含有
a
而若a
在模式串中不存在,我们则可以将窗口移动到
a
出现的位置之后:
Text | | | | | | | | | a | | u | | | | | | |
pattern | | | 不 | 包 | 含 | a | | |
发生不匹配,且pattern
中不含有
a
这样做的意义很明显:
text | | | | | | | | | a | | u | | | | | | | |||
pattern | | a | | 不 | 包 | 含 | a | | 1 | |||||||||||
| a | | 不 | 包 | 含 | a | | 2 | ||||||||||||
| a | | 不 | 包 | 含 | a | | 3 | ||||||||||||
| a | | 不 | 包 | 含 | a | | 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 | | | | |
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 | | | | | | | |
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 |
仅末位匹配时的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 |
末两位匹配的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 |
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')
可以验证这个过程。
相关文章推荐
- Boyer-Moore高质量实现代码详解与算法详解
- 为什么JDK中String类的indexof不使用KMP或者Boyer-Moore等时间复杂度低的算法编辑器
- 字符串匹配算法,Boyer-Moore 算法
- 字符串匹配算法之"Boyer Moore"
- Boyer-Moore 算法 与KMP算法的对比
- BM算法的改进的算法SUNDAY--Boyer-Moore-Horspool-Sunday Aglorithm
- 字符串匹配算法 之 BM(Boyer-Moore)
- (转自阮一峰)Boyer-Moore 算法
- 摩尔投票算法(Boyer–Moore majority vote algorithm)
- 数据结构与算法--Boyer-Moore和Rabin-Karp子字符串查找
- Boyer-Moore(BM)算法学习
- 字符串匹配算法之Boyer-Moore-Horspool Algorithm
- 字符串匹配算法之KMP&Boyer-Moore
- 字符串匹配的 Boyer-Moore 算法
- 字符串匹配算法 之 (Horspool )Boyer-Moore-Horspool
- 摩尔投票算法( Boyer-Moore Voting Algorithm)
- 字符串查找算法总结(暴力匹配、KMP 算法、Boyer-Moore 算法和 Sunday 算法)
- 理解字符串 Boyer-Moore 算法
- 字符串查找算法BM算法(Boyer-Moore)算法
- Boyer-Moore文本匹配算法(联合使用KMP和Horspool算法)