浅谈字符串匹配算法 —— BM算法

概述

BF算法在某些极端情况下,性能会退化的比较严重。

RK 算法需要用到哈希算法,设计一个可以应对各种类型字符的哈希算法则并不简单。

BM算法

BM(Boyer-Moore)算法是一种非常高效的字符串匹配算法,性能约是著名的KMP 算法的 3 到 4 倍。

但是BM算法的实现原理也很复杂。

BM算法的思想

我们把模式串和主串的匹配过程,可以看作模式串在主串中不停地往后滑动。

当遇到不匹配的字符时,BF 算法和 RK 算法的做法是,模式串往后滑动一位,然后从模式串的第一个字符开始重新匹配。

但是,上图的例子,主串中的 “c” ,在模式串中是不存在的。模式串向后滑动的时候,只要 c 与模式串有重合,肯定无法匹配。

所以,我们可以一次性把模式串往后多滑动几位,把模式串移动到 c 的后面。

这样就将模式串向后多滑动了几位,这样一次性往后多滑动几位,匹配的效率其实就提高了。

在什么样的情况下,可以将模式串多滑动,多滑动几位?有什么样的规律吗?

BM算法本质上其实就是在寻找这种规律。

借助这种规律,在模式串与主串匹配的过程中,当模式串和主串某个字符不匹配的时候,能够跳过一些肯定不会匹配的情况。

将模式串往后多滑动几位,进而提高了效率

BM 算法核心思想:利用模式串本身的特点,在模式串中某个字符与主串不能匹配的时候,将模式串往后多滑动几位,以此来减少不必要的字符比较,提高匹配的效率。

BM算法原理

要了解BM算法,其实BM算法包含两个部分,分别是坏字符规则(bad character rule)和好后缀规则(good suffix shift)。

坏字符规则

BF和RK算法都是按模式串的下标从小到大的顺序,依次与主串中的字符进行匹配的。

而 BM 算法的匹配顺序比较特别,它是按照模式串下标从大到小的顺序,倒着匹配的。

我们从模式串的末尾往前倒着匹配,当我们发现某个字符没法匹配的时候,我们把这个没有匹配的字符叫作坏字符

坏字符指的是主串中的字符

拿坏字符 c 在模式串中查找,发现模式串中并不存在这个字符,也就是说,字符 c 与模式串中的任何字符都不可能匹配。

这个时候,我们可以将模式串直接往后滑动三位,将模式串滑动到 c 后面的位置,再从模式串的末尾字符开始比较。

此时模式串中的 “d”,还是无法跟主串中的 a 匹配,这种情况下还能简单的将模式串往后滑动三位吗?

是不行的,因为此时的坏字符 a 在模式串中是存在的,模式串中下标是 0 的位置也是字符 a。

这种情况下,我们可以将模式串往后滑动两位,让两个 a 上下对齐,然后再从模式串的末尾字符开始,重新匹配。

这两种情况分别是坏字符在模式串存在或不存在的情况。具体滑动多少,有没有什么规律呢?

当发生不匹配的时候,我们把坏字符对应的模式串中的字符下标记作 si

如果坏字符在模式串中存在,我们把这个坏字符在模式串中的下标记作 xi,如果不存在,我们把 xi 记作 -1。

那模式串往后移动的位数就等于 si-xi。

这里所说的下标是字符在模式串的下标。

除了这两种情况之外其实还有一种情况,就是坏字符在模式串中不仅存在,还有多个。

如果坏字符在模式串里多处出现,则我们在计算 xi 的时候,选择最靠后的那个。

因为这样不会让模式串滑动过多,导致本来可能匹配的情况被滑动略过。 

好后缀规则

好后缀规则的思路和坏字符规则思路很类似。

当模式串滑动到图中的位置的时候,模式串和主串有 2 个字符是匹配的,倒数第 3 个字符发生了不匹配的情况。

可以通过坏字符规则来计算滑动的位置,也可以利用好后缀规则。

我们把已经匹配的 bc 叫作好后缀,记作{u}。

这里也分成两种情况来看。

我们拿它在模式串中查找,如果找到了另一个跟{u}相匹配的子串{u*},我们就将模式串滑动到子串{u*}与主串中{u}对齐的位置。

如果在模式串中找不到另一个等于{u}的子串,我们就直接将模式串,滑动到主串中{u}的后面。

因为之前的任何一次往后滑动,都没有匹配主串中{u}的情况。

这个处理类似于坏字符规则的处理,这样滑动是否会有点太过头?

这里面 bc 是好后缀,尽管在模式串中没有另外一个相匹配的子串{u*}。

但是如果我们将模式串移动到好后缀的后面,那就会错过模式串和主串可以匹配的情况。

如果好后缀在模式串中不存在可匹配的子串,那在我们一步一步往后滑动模式串的过程中,只要主串中的{u}与模式串有重合,那肯定就无法完全匹配。

但是当模式串滑动到前缀与主串中{u}的后缀有部分重合的时候,并且重合的部分相等的时候,就有可能会存在完全匹配的情况。

之前的例子就是没有考虑到这种情况而过度滑动。

针对这种情况,我们不仅要看好后缀在模式串中,是否有另一个匹配的子串。

我们还要考察好后缀的后缀子串,是否存在跟模式串的前缀子串匹配的。

所谓某个字符串 s 的后缀子串,就是最后一个字符跟 s 对齐的子串,比如 abc 的后缀子串就包括 c, bc。

所谓前缀子串,就是起始字符跟 s 对齐的子串,比如 abc 的前缀子串有 a,ab。

我们从好后缀的后缀子串中,找一个最长的并且能跟模式串的前缀子串匹配的,假设是{v}

如何选择规则?

根据坏字符规则,计算得到的往后滑动的位数,有可能是负数。

PS:为什么会出现负数?

当遇到坏字符时,要计算往后移动的位数 si-xi,其中 xi 的计算是重点,我们如何求得 xi 呢?

或者说,如何查找坏字符在模式串中出现的位置呢?

如果在模式串中顺序遍历查找,这样会比较低效。

为了提高效率,可以使用散列表。

散列表记录不同字符在模式串中“最后出现的位置”。并不是 si 的位置往前查找的第一个位置。

就比如之前的这个图,利用坏字符规则的话,si = 4 ,而c在模式串最后出现的位置 xi = 6 ,坏字符规则 si - xi = -2

所以会出现 xi 大于 si 的情况,即计算出滑动的位数为负数

我们可以分别计算好后缀和坏字符往后滑动的位数,然后取两个数中最大的,作为模式串往后滑动的位数。

这样也可以避免掉坏字符规则计算出滑动位数有可能是负数的情况。

关于BM算法的吐槽

BM算法的原理相比于BM和RK来讲要复杂很多很多,但是通过学习也是可以理解的,但是具体的代码实现就非常不容易了

关于BM算法学习的话还是着重于思想和原理,有位老哥总结的非常好

我特此引用一下,作为我学习BM算法的点

1、要有优化意识,BF,RK 算法已经能够满足我们需求了,为什么发明 BM 算法?是为了减少时间复杂度,但是带来的弊端是,优化代码变得复杂,维护成本变高。

2、需要查找,需要减少时间复杂度,应该想到什么?散列表。

3、如果某个表达式计算开销比较大,又需要频繁的使用怎么办?预处理,并缓存。

BM算法的性能很高,正所谓为了性能,就需要更复杂的算法,但是越复杂的算法,代码实现肯定就越复杂,细节就越容易出错
 

关于BM算法的性能,已有论文证明在最坏情况下BM 算法的比较次数上限是 3n。

特此记录,有个了解

发布了113 篇原创文章 · 获赞 25 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_42006733/article/details/105136180