今日头条【进击算法:字符串匹配的 BM 算法】

1,202 阅读4分钟
原文链接: m.toutiaocdn.cn

进击算法:字符串匹配的 BM 算法

BM 算法介绍

各种文本编辑器的 "查找" 功能(Ctrl+F),大多采用 Boyer-Moore 算法。

进击算法:字符串匹配的 BM 算法

Boyer-Moore 算法不仅效率高,而且构思巧妙,容易理解。1977 年,德克萨斯大学的 Robert S. Boyer 教授和 J Strother Moore 教授发明了这种算法。

下面我根据书籍 Algorithms on Strings, Trees, and Sequences 的第 2 章 Chapter 2 - Exact Matching: Classical Comparison-Based Methods 来介绍 BM 算法。

好后缀

假设匹配过程中发现 x[i]=a 和 y[i+j] = b 不同,此时当前匹配的信息有:

x[i+1 .. m-1]=y[i+j+1 .. j+m-1]=u

x[i] != y[i+j]

此时我们假设能找到 u 在 x 中的最右出现位置,则可以直接将 x 向右滑动 shift 距离。

进击算法:字符串匹配的 BM 算法

but,如果我们没在 x 中找到 u,则我们尝试去找到 y[i+j+1 .. j+m-1] 的最长后缀 v,同时 v 也是 x 的前缀。

进击算法:字符串匹配的 BM 算法

总结下上面两种情况:

  • u 可以完整的再次出现在 x 中

  • u 的后缀是 x 的前缀

坏字符

进击算法:字符串匹配的 BM 算法

我们找到 y[i+j]=b 在 x 中最右出现的位置,如果没找到直接左对齐 y[i+j+1]:

进击算法:字符串匹配的 BM 算法

我们可以发现,坏字符的情况中,有可能 shift 是负数。

移动

我们可以根据上面的 好后缀和坏字符分别计算出 shift(好后缀) 和 shift(坏字符) ,我们最后真正移动的 shift 则是 max(shift(好后缀),shift(坏字符))。

算法实现

下面我们来分别计算 shift(好后缀) 和 shift(坏字符)。

先来求 shift(坏字符),具体算法如下:

进击算法:字符串匹配的 BM 算法

上面图中第一个说明是尾部不匹配的时候,我们查找字符 a 在 pattern 中的位置,假设是 i,则 Pattern shift 的距离是 n-i

第二是是说如果失配发生在 pattern 中第 j 个位置,此时字符 a 在 pattern 中的位置为 i,此时 shift 为 j-i,此时意味着,如果的话,此时我们只能取 shift=1,下面我们来计算

进击算法:字符串匹配的 BM 算法

下面我们来看好后缀怎么算计:

进击算法:字符串匹配的 BM 算法

先看上图,我们定义 L(i) 为最大的小于 n 的位置,满足 P[i..n] 是 P[1..L(i)] 的后缀。

接着我们定义 L'(i),其含义如上图,我们在 L(i) 的基础上,定义 P[i-1] != P[L'(i)-n+i-1]。

举个例子:

进击算法:字符串匹配的 BM 算法

接着我们定义为 P[1..j] 和 P[1..n] 最长公共后缀。

我们来看下定义 P[1..j],假设存在 i 满足 L‘(i)=j,即 P[i..n] 是同后缀,并且 P[i-1]!=P[j-n+i-1] 也不同,即,此时 L'(i)=j,于是我们就有了下面的算法:

进击算法:字符串匹配的 BM 算法

上面算法中我们是假设已经知道了 Nj(P) 了,然后通过 Nj 来计算出 L'(i),那我们怎么计算 Nj 呢?

进击算法:字符串匹配的 BM 算法

计算完 Nj,下面计算 L':

进击算法:字符串匹配的 BM 算法

下面我们看另一种情况,当我们找不到后缀的时候,即 L'(i)=0,我们可以退而求其次,求前缀,看下图:

进击算法:字符串匹配的 BM 算法

我们定义 l'(i) 是 P[i..n] 的最长后缀同时也是 P[1..n] 的前缀,如果不存在这样子的前缀,则 l'[i] = 0,此时的含义是说,此时 shift=n,为什么移动最大呢?因为我们先去找 Patten 中是否存在 P[i..n],因为如果要匹配,则 pattern 中必须要存在 P[1..L'(i)],但是不幸的是没找到,这个时候我们可以直接先 shift i-1,然后在慢慢右移,直到 l'(i),这个过程如下图:

进击算法:字符串匹配的 BM 算法

下面就是怎么去计算 l'(i) 了。

我们如果假设 l'(i) 存在,即 l'(i) = j>0,那么此时 Nj(P) = j,并且此时 j 肯定小于等于 |P[i..n]| = n - i + 1,有了这么个洞见以后,我们再来看怎么计算 l'(i).

假设 N[j] == j, j<=n-i+1 => i<=n-j+1,此时 j 越大,i 越小,因此我们就可以这么做了:

进击算法:字符串匹配的 BM 算法

代码如下:

进击算法:字符串匹配的 BM 算法

现在我们来看下怎么根据 L'(i) 和 l'(i) 来计算 shift 的距离:当 P[i-1] != T[k] 的时候,如果 L'(i) > 0,则移动 n - L'(i),否则移动 n - l'(i),此处需要注意一个特殊情况,即 i=n 的时候,我们需要移动 1 位。

代码如下:

进击算法:字符串匹配的 BM 算法

好了,现在一切就绪,我们开始整个搜索过程了,完整的搜索代码见 github 地址 。

另外一个完整搜索过程的图示可以看 search examples。

由于头条上Markdown支持不是太好,原文可以看:https://www.zybuluo.com/zhuanxu/note/1035645