我们在字符串匹配算法(一)学习了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算法详解的更多相关文章

  1. 字符串匹配算法之BM算法

    BM算法,全称是Boyer-Moore算法,1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法. BM算法定义了两个规则: ...

  2. BM算法详解

    http://www-igm.univ-mlv.fr/~lecroq/string/node14.html http://www.cs.utexas.edu/users/moore/publicati ...

  3. 字符串匹配算法(三)-KMP算法

    今天我们来聊一下字符串匹配算法里最著名的算法-KMP算法,KMP算法的全称是 Knuth Morris Pratt 算法,是根据三位作者(D.E.Knuth,J.H.Morris 和 V.R.Prat ...

  4. BM算法  Boyer-Moore高质量实现代码详解与算法详解

    Boyer-Moore高质量实现代码详解与算法详解 鉴于我见到对算法本身分析非常透彻的文章以及实现的非常精巧的文章,所以就转载了,本文的贡献在于将两者结合起来,方便大家了解代码实现! 算法详解转自:h ...

  5. 算法进阶面试题01——KMP算法详解、输出含两次原子串的最短串、判断T1是否包含T2子树、Manacher算法详解、使字符串成为最短回文串

    1.KMP算法详解与应用 子序列:可以连续可以不连续. 子数组/串:要连续 暴力方法:逐个位置比对. KMP:让前面的,指导后面. 概念建设: d的最长前缀与最长后缀的匹配长度为3.(前缀不能到最后一 ...

  6. 数据结构4.3_字符串模式匹配——KMP算法详解

    next数组表示字符串前后缀匹配的最大长度.是KMP算法的精髓所在.可以起到决定模式字符串右移多少长度以达到跳跃式匹配的高效模式. 以下是对next数组的解释: 如何求next数组: 相关链接:按顺序 ...

  7. 安全体系(二)——RSA算法详解

    本文主要讲述RSA算法使用的基本数学知识.秘钥的计算过程以及加密和解密的过程. 安全体系(零)—— 加解密算法.消息摘要.消息认证技术.数字签名与公钥证书 安全体系(一)—— DES算法详解 1.概述 ...

  8. 【转】AC算法详解

    原文转自:http://blog.csdn.net/joylnwang/article/details/6793192 AC算法是Alfred V.Aho(<编译原理>(龙书)的作者),和 ...

  9. kmp算法详解

    转自:http://blog.csdn.net/ddupd/article/details/19899263 KMP算法详解 KMP算法简介: KMP算法是一种高效的字符串匹配算法,关于字符串匹配最简 ...

随机推荐

  1. Linkerd 2.10(Step by Step)—多集群通信

    Linkerd 2.10 系列 快速上手 Linkerd v2.10 Service Mesh(服务网格) 腾讯云 K8S 集群实战 Service Mesh-Linkerd2 & Traef ...

  2. 深入理解JVM,7种垃圾收集器,看完我跪了

    如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现.Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商.版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并 ...

  3. csps前小结

    冒着题没改完颓废被发现的风险来写博客 好像离csps只剩两天了,然而没啥感觉 最近考试有时考得还算可以,有时也会很炸 今天考试事实上心态啥崩,因为T1结论题一直没思路,想了一个小时连暴力都没打 过了一 ...

  4. JDK并发包一

    JDK并发包一 同步控制是并发程序必不可少的重要手段,synchronized是一种最简单的控制方法.但JDK提供了更加强大的方法----重入锁 重入锁 重入锁使用java.util.concurre ...

  5. C程序从编译到运行

    第一篇文章 一.前言 最近在看CSAPP(深入理解计算机系统)然后以前也学过C语言,但是从来没有深究写好的C代码是怎么编译再到执行的. 所以现在自己学习,然后记录下来. 以最常用的hello worl ...

  6. 利用 V8 深入理解 JavaScript 设计

    JavaScript 代码运行 以大家开发常用的 chrome 浏览器或 Node 举例,我们的 JavaScript 代码是通过 V8 运行的.但 V8 是怎么执行代码的呢?当我们输入 const ...

  7. redis 客户端实现读写分离实现

    背景 (1) redis单机的读写性能轻松上大几万,不过线上环境不会只部署光秃秃的一个节点,还是会配合 sentinel 再部署一个 slave作为高可用节点的: 但是standby的slave节点是 ...

  8. 26、mysqlsla慢查询日志分析工具

    1.介绍: mysqlsla是hackmysql.com推出的一款MySQL的日志分析工具,可以分析mysql的慢查询日志.分析慢查询非常好用,能针 对库分析慢查询语句的执行频率.扫描的数据量.消耗时 ...

  9. Linux基础 -01

    01Linux快速入门 1.计算机组成原理 1.1什么是计算机 计算机一般被称为"电脑",即通电的大脑 电脑二字蕴含了人类对计算机的终极期望; 希望它能像人脑一样为我们工作,从而取 ...

  10. Vue 两个字段联合校验典型例子--修改密码

    1.前言   本文是前文<Vue Element-ui表单校验规则,你掌握了哪些?>针对多字段联合校验的典型应用.   在修改密码时,一般需要确认两次密码一致,涉及2个属性字段.类似的涉及 ...