1、概述
\quad \quad BM(Boyer-Moore) 算法,是一种非常高效的字符串匹配算法。据实验统计,它的性能是著名的 KMP 算法的 3 到 4 倍,文本量越大BM算法的效果越明显。各种文本编辑器的"查找"功能(Ctrl+F),大多采用Boyer-Moore算法
2、原理
\quad \quad BM算法是从后往前进行主串与模式串的比,通过两张表来改进移动的距离。当主串与模式串的字符不匹配时,模式串往后滑动的长度是基于坏字符规则和好后缀规则决定的,那个规则往后移动的长度长,就基于哪一个长度移动。因此,可以预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下,选最大者就可以了。
2.1 坏字符规则
\quad \quad “坏字符” 是什么意思?就是指模式串和子串当中不匹配的字符。比如,下图,字符T就是坏字符。
坏字符规则:
\quad \quad
当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,后移位数 = 坏字符对应在模式串中的位置 - 坏字符在模式串中最右出现的位置。
注:
- 如果"坏字符"不包含在模式串中,我们就记最右出现位置为-1。
- 位置是从0开始记的
由来:
\quad \quad
文本串的坏字符在模式串中有三种情况,下面分情况讨论模式串移动情况
1、 模式串中有对应的坏字符,仅一次
- BM算法是从模式串的末尾开始匹配的,我们发现末尾主串字符T与模式串字符G不匹配,则字符
T
为坏字符。既然最后一位不匹配了,那肯定是匹配不成功了,模式串应往后移动。又因T
包含在模式串中,我们就将模式串当中的字符T和主串的坏字符对齐。往后移动的长度=8-1=7。【因为”T
“这个坏字符对应着着模式串的第8位(从零开始编号),且在模式串中的出现位置为1)
2、 模式串中有对应的坏字符,多次出现
- 模式串中多次出现坏字符时,让模式串中 最靠右 的对应字符与坏字符相对。
- 为什么要最靠右呢?
- 假设让上图中坏字符与模式串中第一个出现位置即左边对应,则会出现漏匹配情况。
- 而与最靠右的对应,就不会漏掉了
3、 模式串中没有对应的坏字符,多次出现
- 如果坏字符在模式串中不存在,直接把模式串挪到主串坏字符的下一位
- 首先,“文本串"与"模式串"头部对齐,从尾部开始比较。”T“与”G“不匹配。这时,”
T
“就被称为"坏字符”(bad character),即不匹配的字符,它对应着模式串的第8位。且"T
“不包含在模式串中(相当于最右出现位置是-1),这意味着可以把模式串后移8-(-1)=9位,从而直接移到”T
"的后一位。
2.2 好后缀规则
\quad \quad “好后缀” 又是什么意思?就是指模式串和子串当中相匹配的后缀。
好后缀规则:
\quad \quad
当字符失配时,后移位数 = 好后缀对应在模式串中的位置 - 好后缀在模式串上一次出现的最右位置
。
注:
- "好后缀"的位置以最后一个字符为准。假定"ABCDEF"的"EF"是好后缀,则它的位置以"F"为准,即5(从0开始计算)。
- 如果"好后缀"在模式串中只出现一次,则它的上一次出现位置为 -1。比如,"EF"在"ABCDEF"之中只出现一次,则它的上一次出现位置为-1(即未出现)。
- 如果"好后缀"有多个,则除了最长的那个"好后缀",其他"好后缀"的上一次出现位置必须在头部。比如,假定"BABCDAB"的"好后缀"是"DAB"、“AB”、“B”,请问这时"好后缀"的上一次出现位置是什么?回答是,此时采用的好后缀是"B",它的上一次出现位置是头部,即第0位。这个规则也可以这样表达:如果最长的那个"好后缀"只出现一次,则可以把搜索词改写成如下形式进行位置计算"(DA)BABCDAB",即虚拟加入最前面的"DA"。
由来:
- 如果模式串中存在已经匹配成功的好后缀,则把目标串与好后缀对齐,然后从模式串的最尾元素开始往前匹配。
- 如果完全不存在和好后缀匹配的子串,则右移整个模式串。
3、代码实现
3.1 坏字符表
- 记录模式串中每一个字符出现在模式串最右的位置(位置从0开始记起)
- 其中,由于BM算法是从末尾开始比较的,所以坏字符在模式串中最右出现的位置是不包括模式串最后一个字符。
#坏字符表
def getBMBC(T):
'''
以字典的形式表示坏字符表,
优点在于每遍历一个字符,其值会取代前面记录的
到最后就会记录最右边的位置
'''
BMBC=dict( )
#不包括模式串末尾字符
for i in range(len(T)-1):
char=T[i]
#记录坏字符最右位置
BMBC[char]=i
return BMBC
if __name__=="__main__":
t="abcdabd"
print(getBMBC(t))
#结果: {'a': 4, 'b': 5, 'c': 2, 'd': 3}
3.2 好后缀表
- 记录每一个好后缀后移位数
后移位数 = 好后缀对应在模式串中的位置 - 好后缀在模式串上一次出现的最右位置
。- 不能构成好后缀的后移位数为0
#好后缀表
def getBMGS(T):
BMGS=dict( )
# 无后缀仅根据坏字移位符规则
BMGS['']=0
for i in range(len(T)):
#好后缀
GS=T[len(T)-i-1:]
for j in range(len(T)-i-1):
#匹配部分
NGS=T[j:j+i+1]
if GS==NGS:
BMGS[GS]=len(T)-j-i-1
return BMGS
if __name__=="__main__":
t="bcabab"
print(getBMGS(t))
#结果:{'': 0, 'b': 2, 'ab': 2}
3.3 BM算法
- 计算模式串的坏字符表,好后缀表
- 计算移动长度
- 取其最大者进行后移,并重新比较
#坏字符表
def getBMBC(T):
'''
以字典的形式表示坏字符表,
优点在于每遍历一个字符,其值会取代前面记录的
到最后就会记录最右边的位置
'''
BMBC=dict( )
#不包括模式串末尾字符
for i in range(len(T)-1):
char=T[i]
#记录坏字符最右位置
BMBC[char]=i
return BMBC
#好后缀表
def getBMGS(T):
BMGS=dict( )
# 无后缀仅根据坏字移位符规则
BMGS['']=0
for i in range(len(T)):
#好后缀
GS=T[len(T)-i-1:]
for j in range(len(T)-i-1):
#匹配部分
NGS=T[j:j+i+1]
if GS==NGS:
BMGS[GS]=len(T)-j-i-1
return BMGS
# BM算法
def BM(S,T):
m=len(T)
n=len(S)
i=0
j=m
indies=[]
BMBC=getBMBC(T)#坏字符表
BMGS=getBMGS(T)#好后缀表
while i < n:
while j>0:
if i+j-1>=n:#当无法继续向下搜索就返回-1
return indies
#主串判断匹配部分
a=S[i+j-1:i+m]
#模式串匹配部分
b=T[j-1:]
#如果当前位匹配成功则继续匹配
if a==b:
j=j-1
#如果当前位匹配失败根据规则最大者移位
else:
i=i+max(BMGS.setdefault(b[1:],m),j-BMBC.setdefault(S[i+j-1],0))
j=m
if j==0:
indies.append(i)
i+=1
j=len(T)
if __name__=="__main__":
s="abcabaabcabacbaab"
t="baab"
b=BM(s,t)
print(b)
# 结果:[4, 13]
参考资料:
1、https://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html