字符串匹配算法(二)-BM算法详解
我们在字符串匹配算法(一)学习了BF算法和RK算法,那有没更加高效的字符串匹配算法呢。我们今天就来聊一聊BM算法。
BM算法
我们把模式串和主串的匹配过程,可以看做是固定主串,然后模式串不断在往后滑动的过程。当遇到不匹配的字符时,BF算和RK算法的做法是,把模式串向后滑动一位,然后从模式串的第一位开始重新匹配。如下图所示。
由于BF算法和RK算法,在遇到不匹配的字符时,模式串只是向后滑动一位,这样的话时间复杂度比较高,那有没有什么算法可以一下子多滑动几位呢?比如遇到主串A中的字符d,由于d不在模式串中,所以只要d和模式串有重合,那就肯定不能匹配。所以我们可以直接多滑动几位,直接滑到d的后面,然后再继续匹配,这样不就提高了效率了吗?
今天要聊的BM算法,本质上就是寻找这种规律。借助这种规律,在模式串和主串匹配的过程中,当模式串和主串遇到不匹配的字符时,能够跳过一些肯定不匹配的情况,多往后滑动几位。
BM算法的原理
BM算法包含2部分,分别是坏字符规则和好后缀规则。
1.坏字符规则
我们在BF算法和RK算法中,在模式串和主串匹配的过程中,我们都是按模式串的下标从小到大的顺序依次匹配的。而BM算法的匹配顺序则相反,是从大到小匹配的。如下所示。
从模式串的末尾倒着匹配,当发现主串中某个字符匹配不上时,我们就把这个字符称为坏字符。我们拿着坏字符d在模式串中查找,发现d不在模式串中。这个时候,我们可以将模式串直接滑动3位,滑动到字符d的后面,然后再从模式串的末尾开始比较。
这个时候,我们发现主串中的字符串b和模式串的中的c不匹配。这个时候由于坏字符b在模式串中是存在的,模式串中下标为1的位置也是字符b,所以我们可以把模式串向后滑动1位,让主串中的b和模式串中的b相对齐。然后再从模式串的末尾字符开始重新进行匹配。
从上面的例子中,我们可以总结出规律。当发生不匹配的时候,我们把坏字符对应的模式串中的字符下标记做Ai。如果坏字符在模式串中存在,我们把这个坏字符在模式串中的下标记做Bi(如果坏字符在模式串中出现多次,我们把靠后的那个位置记做是Bi,这么做是为了不让模式串向后滑动过多,导致可能匹配的情况错过)。那模式串向后滑动的位数就是Ai-Bi。
不过单纯的使用坏字符规则是不够的。因为根据Ai-Bi计算出来的移动位数有可能是负数。比如主串是aaaaaa,模式串是baaa。所以,BM算法还需要用到“好后缀规则”。
2.好后缀规则
好后缀规则和坏字符规则思路上很相似。如下图所示。
当模式串滑动到图中的位置时,模式串和主串有2个字符是匹配的,倒数第三个字符发生了不匹配的情况。我们把已经匹配的ab叫做好后缀,记做{u}。我们拿它在模式串中进行寻找另一个和{u}相匹配的子串{u*}。那我们就将模式串滑动到子串{u*}和主串{u}对齐的位置。
如果在模式串中找不到另一个等于{u}的子串,我们就直接将模式串,滑动到主串{u}的后面。因为之前的任何一次往后滑动,都没有匹配主串{u}的情况。不过,当模式串中不存在等于{u}的子串时,我们直接将模式串滑动到{u}的后面,这样是否会错过可能匹配的情况呢。如下所示,这里的ab是好后缀,尽管在模式串中没有另一个相匹配的子串{u*},但如果我们将模式串移动到{u}的后面,那就错过了模式串和主串相匹配的情况。
所以,当模式串滑动到前缀与主串中{u}的后缀有部分重合的时候,并且重合的部分相等的时候,就有可能会存在完全匹配的情况。针对这种情况,我们不仅要看好后缀在模式串中,是否存在另一个匹配的子串。我们还要考察好后缀的后缀子串,是否和模式串的前缀子串相匹配。
这里我们再来解释一下字符串的后缀子串和前缀子串。所谓字符串A的后缀子串,就是最后一个字符跟A对齐的子串,比如,字符串abc的后缀子串是c、bc。所谓的前缀子串,就是起始字符和A对齐的子串。比如,字符串abc的前缀子串是a、ab。我们从好后缀子串中,找一个最长并且能和模式串前缀子串匹配的,假如是{v}。然后滑动到如图所示的位置。
到目前位置,我们的坏字符和好后缀就讲完了,我们接下来想这么一个问题。当模式串和主串中某个字符不匹配的时候,我们是选好后缀规则呢还是坏字符规则来计算向后滑动的位数呢?
我们可以分别计算坏字符规则和好后缀规则向后滑动的位数,然后取两个数的最大的,作为模式串往后滑动的位数。
BM算法的代码实现
我们接下来来看BM算法的代码是如何实现的。
"坏字符规则"中当遇到坏字符时,我们要计算后移的位数Ai-Bi,其中Bi的计算的重点。如果我们拿坏字符在模式串中顺序查找,这样是可以实现,不过效率比较低下。我们可以用大小256的数组,来记录每个字符在模式串中出现的位置。数组的下标对应的是字符的Ascii编码,数组中存储的是这个字符在模式串中的位置。
SIZE = 256
def generateBC(b, m):
bc=[-1]*SIZE
for i in range(m):
accii=ord(b[i])
bc[accii]=i
return b
我们先把BM算法中的“坏字符规则”写好,先不考虑“好后缀规则”,并且忽略掉Ai-Bi为负数的情况。
def bm(a,n,b,m):
#生成bc散列表
bc=generateBC(b,m)
#i表示主串与模式串对齐的第一个字符
i = 0
while i<=n-m:
for j in range(m-1,-2,-1): #模式串从后往前匹配
if(a[i+j]!=b[j]): #坏字符对应模式串中的下标是j
break
if(j<0):
#表示匹配成功
return i
#Ai-Bi,等同于向后滑动几位
i = i + (j - bc[ord(a[i+j])])
return -1
到目前为止,我们已经实现了“坏字符规则”的代码框架,剩下就是需要往里面添加“好后缀规则”的代码逻辑了。
在继续讲解之前,我们先简单回顾一下,前面讲的“好后缀规则”中最关键的内容。
在模式串中,查找和好后缀匹配的另一个子串。
在好后缀的后缀子串中,查找最长的,能跟模式串前缀子串相匹配的后缀子串。
我们可以这么考虑,因为好后缀也是模式串的后缀子串,所以,我们可以在模式串和主串进行匹配之前,通过预处理模式串,预先计算好模式串中的每个后缀子串,对应的另外一个可匹配子串中的位置。这个预处理过程有点复杂。大家可以多读几遍,在纸上画一画。
我们先来看如何表示模式串中的不同后缀子串呢?因为后缀子串的最后一个字符的位置是固定的,下标为m-1,所以我们只需要记录长度就可以了。通过长度,我们可以唯一确定一个后缀子串。
下面我们引入一个关键的数组suffix。suffix数组的下标k,表示后缀子串的长度,数组对应的位置存储的是,在模式串中和好后缀{u}相匹配的子串{u*}的起始下标位置。
其中有一个点需要注意,如果模式串中有多个子串跟后缀子串相匹配,我们选最靠后的那个子串的起始位置,以免滑动的太远,错过可能匹配的情况。
接下来我们来看好后缀规则的第二条,就是要在好后缀的后缀子串中,查找最长的能跟模式串前缀子串匹配的后缀子串。接来下,我们来引入另一个数组变量prefix,来记录模式串的后缀子串是否能匹配模式串的前缀子串。
接下来我们来看如何给这两个数组赋值呢?我们拿下标为0~i的子串(i 可以是 0 到 m-2)与整个模式串,求公共后缀子串。如果公共后缀子串的长度是 k,那我们就记录 suffix[k]=j(j 表示公共后缀子串的起始下标)。如果 j 等于 0,也就是说,公共后缀子串也是模式串的前缀子串,我们就记录 prefix[k]=true。我们来看代码实现。
def generateSP(b,m):
suffix= [-1]*m
prefix= [False]*m
for i in range(m-1):
j=i
k=0 #公共后缀子串长度
while (j>=0 and b[j]==b[m-1-k]):
j=j-1
k=k+1
#j+1表示公共后缀子串在b[0, i]中的起始下标
suffix[k]=j+1
if (j==-1):
#如果公共后缀子串也是模式串的前缀子串
prefix[k] = True
return suffix,prefix
下面我们来看如何根据好后缀规则,来计算模式串往后滑动的位数?假设好后缀的长度是 k。我们先拿好后缀,在 suffix 数组中查找其匹配的子串。如果 suffix[k]不等于 -1(-1 表示不存在匹配的子串),那我们就将模式串往后移动 j-suffix[k]+1 位(j 表示坏字符对应的模式串中的字符下标)。如果 suffix[k]等于 -1,表示模式串中不存在另一个跟好后缀匹配的子串片段。我们就用下面这条规则来处理。好后缀的后缀子串 b[r, m-1](其中,r 取值从 j+2 到 m-1)的长度 k=m-r,如果 prefix[k]等于 true,表示长度为 k 的后缀子串,有可匹配的前缀子串,这样我们可以把模式串后移 r 位。如果两条规则都没有找到可以匹配的好后缀及其后缀子串的后缀子串,我们就将整个模式串后移 m 位。到此为止,我们的好后缀规则也聊完了,我们现在把好后缀规则的代码插入到前面的框架中,就可以得到完整版本的BM算法了。
#散列表的大小
SIZE = 256
def generateBC(b, m):
bc=[-1]*SIZE
for i in range(m):
accii=ord(b[i])
bc[accii]=i
return bc
def generateSP(b,m):
suffix= [-1]*m
prefix= [False]*m
for i in range(m-1):
j=i
k=0 #公共后缀子串长度
while (j>=0 and b[j]==b[m-1-k]):
j=j-1
k=k+1
#j+1表示公共后缀子串在b[0, i]中的起始下标
suffix[k]=j+1
if (j==-1):
#如果公共后缀子串也是模式串的前缀子串
prefix[k] = True
return suffix,prefix
#j表示坏字符对应的模式串中的字符下标
#m表示模式串长度
def moveSP(j,m,suffix,prefix):
#好后缀的长度
k=m-1-j
if suffix[k]!=-1:
return j-suffix[k]+1
for r in range(j+2,m):
if prefix[m-r]==True:
return r
return m
def bm(a,n,b,m):
#生成bc散列表
bc=generateBC(b,m)
suffix, prefix = generateSP(b,m)
#i表示主串与模式串对齐的第一个字符
i = 0
while i<=n-m:
for j in range(m-1,-2,-1): #模式串从后往前匹配
if(a[i+j]!=b[j]): #坏字符对应模式串中的下标是j
break
if(j<0):
#表示匹配成功
return i
#Ai-Bi,等同于先后滑动几位
x = j - bc[ord(a[i+j])]
y = 0
#如果有好后缀的话
if j < m-1:
y = moveSP(j, m, suffix, prefix)
i = i + max(x, y)
return -1
到此为止,我们BM算法就聊完。更多硬核知识,请关注公众号。

字符串匹配算法(二)-BM算法详解的更多相关文章
- 字符串匹配算法之BM算法
BM算法,全称是Boyer-Moore算法,1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法. BM算法定义了两个规则: ...
- BM算法详解
http://www-igm.univ-mlv.fr/~lecroq/string/node14.html http://www.cs.utexas.edu/users/moore/publicati ...
- 字符串匹配算法(三)-KMP算法
今天我们来聊一下字符串匹配算法里最著名的算法-KMP算法,KMP算法的全称是 Knuth Morris Pratt 算法,是根据三位作者(D.E.Knuth,J.H.Morris 和 V.R.Prat ...
- BM算法 Boyer-Moore高质量实现代码详解与算法详解
Boyer-Moore高质量实现代码详解与算法详解 鉴于我见到对算法本身分析非常透彻的文章以及实现的非常精巧的文章,所以就转载了,本文的贡献在于将两者结合起来,方便大家了解代码实现! 算法详解转自:h ...
- 算法进阶面试题01——KMP算法详解、输出含两次原子串的最短串、判断T1是否包含T2子树、Manacher算法详解、使字符串成为最短回文串
1.KMP算法详解与应用 子序列:可以连续可以不连续. 子数组/串:要连续 暴力方法:逐个位置比对. KMP:让前面的,指导后面. 概念建设: d的最长前缀与最长后缀的匹配长度为3.(前缀不能到最后一 ...
- 数据结构4.3_字符串模式匹配——KMP算法详解
next数组表示字符串前后缀匹配的最大长度.是KMP算法的精髓所在.可以起到决定模式字符串右移多少长度以达到跳跃式匹配的高效模式. 以下是对next数组的解释: 如何求next数组: 相关链接:按顺序 ...
- 安全体系(二)——RSA算法详解
本文主要讲述RSA算法使用的基本数学知识.秘钥的计算过程以及加密和解密的过程. 安全体系(零)—— 加解密算法.消息摘要.消息认证技术.数字签名与公钥证书 安全体系(一)—— DES算法详解 1.概述 ...
- 【转】AC算法详解
原文转自:http://blog.csdn.net/joylnwang/article/details/6793192 AC算法是Alfred V.Aho(<编译原理>(龙书)的作者),和 ...
- kmp算法详解
转自:http://blog.csdn.net/ddupd/article/details/19899263 KMP算法详解 KMP算法简介: KMP算法是一种高效的字符串匹配算法,关于字符串匹配最简 ...
随机推荐
- 【NX二次开发】Block UI 选择表达式
属性说明 属性 类型 描述 常规 BlockID String 控件ID Enable Logical 是否可操作 Group ...
- 【VBA】MsgBox用法
MsgBox用法: Sub subMsgBox() Dim iok As Byte iok = MsgBox("是否XXX", vbYesNoCancel + vbQuestion ...
- [Linux]经典面试题 - 网络基础 - TCP三次握手
[Linux]经典面试题 - 网络基础 - TCP三次握手 目录 [Linux]经典面试题 - 网络基础 - TCP三次握手 一.TCP报文格式 1.1 TCP报头 1.2 报文图例 二.TCP三次握 ...
- Python变量小秘密
变量全都是引用 跟其他编程语言不同,Python的变量不是盒子,不会存储数据,它们只是引用,就像标签一样,贴在对象上面. 比如: >>> a = [1, 2, 3] >> ...
- Lin语法(Linq对数据库操作)
一.Linq语句的应用: var queryAllCustomers = from cust in customers group cust by cust.City into custGroup ...
- 地图可视化神器keplergl新增对jupyter lab 3.0的支持
就在几天前,地图可视化神器kepler.gl面向Python的接口库keplergl迎来了新的0.3.0版本更新. 虽然官方文档还并未及时更新相关的内容说明,但我在快速地试用之后发现,现在的keple ...
- AIDL —— Android接口定义语言
AIDL:Android Interface Definition Language,即Android接口定义语言,是Android进程间通信比较常用的一种方式.翻译一下,就是为了让某个Service ...
- Redis6使用指导(完整版)
一.Nosql与Redis概述 二.Redis6安装 三.常用五大数据类型 四.Redis6配置文件详解 五.Redis6的发布和订阅 六.Redis6新数据类型 七.Jedis操作Redis6(Ma ...
- drf-Request与Response
一.Request 在Rest Framework 传入视图的request对象已经不再是Django默认的HTTPResponse对象了,而是Rest Framework提供的Request类的对象 ...
- 快速了解ARP
目录 前言 一.MAC 1.MAC地址三种帧 二.ARP 1.五种ARP 三.ARP老化 四.什么时候会发送免费ARP 五.代理ARP 六.ARP欺骗 总结 前言 分别介绍MAC地址和五种ARP报文 ...