字符串匹配BM(Boyer-Moore)算法学习心得

  BM算法 Boyer-Moore算法 的缩写,是一种基于后缀比较的模式串匹配算法。BM算法在最坏情况下可以做到线性的,平均情况下是亚线性的(即低于线性)。这也是他在实际应用中优于KMP算法的一个原因吧。

  最近突然在看[柔性字符串匹配].[Flexible.Pattern.Matching.in.Strings](Gonzalo.Navarro,.Mathieu.Raffinot),想了解一下字符串匹配的一些算法,以前使用的算法基本都只是和ACM有关的,而且在单串匹配的情况下一般都只用KMP算法,而在这本书中对KMP算法的评价并不是很好,原因主要是出于工程上的“实用”上的原因吧。KMP算法虽然在理论上最坏复杂度也是线性的,可是在实际应用中并没有那么多最坏情况,而且如果用于模式匹配的话,很多情况下模式串也是很短的。

  下面来说一说我写这个的原因吧,我搞了两年ACM嘛,现在看书肯定还是和ACM有一定关系的,而且很自然的会将一些事情和ACM中做对比的。[柔性字符串匹配]说KMP在实际应用中不好,那我就想反过来看一下在实际应用中比较好的算法在ACM中效果怎么样了,最先看到的就是BM算法(当然在书中还看到了Shift-or 和Shift-and 算法,但由于长度限制差别太大,实在不具可比性,不过这两个算法个人感觉是相当优雅的)。由于这本书太实用了,对于“不实用”的算法都只做基本介绍(对于KMP算法也一样,只做了一点大概的介绍,其实我非常不明白为什么会出现这种现象,难道理论和应用的差别有这么大么?)。书上说原始BM算法在最坏情况下复杂度是O(n*m)的,O(n*m)的算法在ACM中必然是不“实用”的,但它说BM算法有两个优化版本可以在最坏情况下也能达到线性的。这样一来我觉得就有一定可比性了,于是就想仔细看一下BM算法。

  书上说的两个算法是:the Boyer-Moore-Galil [Gal79] and the Turbo-BM[CGR92] algorithms。

  我找了找,也没找到什么相关资料,但是回头一想这个算法都出来这么久了,直接搜BM算法应该也能搜出优化过的BM算法吧。

  下面是我找到的一些文章。

 

字符串匹配那些事(一)

  这篇文章好像是淘宝的吧,其中没有明确指出BM算法的时间复杂度。

M模式匹配算法原理(图解)

  这篇文章对BM进行了大致的讲解,文中分析到了时间复杂度,

  文中为:最好情况下的时间复杂度为O(n/m),最坏情况下时间复杂度为O(m·n)。

精确字符串匹配(BM算法)  BM 算法中“好后缀”预处理

  在第一篇文章中提到了时间复杂度:整个算法的时间复杂度最坏的情况是 O(m),m 是 T 的长度。

  可有一点我不明白,按这篇文章中的作法,好像并没有对原始的BM算法进行优化,而原始的BM算法确实是O(m·n)的。

 

  又重新去搜论文了。

A Fast String Searching Algorithm - The University of Texas at Austin

Boyer-Moore-Galil [Gal79] Z. Galil. On improving the worst case running time of the Boyer-Moore string searching algorithm. Communications of the ACM, 22(9):505–508, 1979.

  看了一下这两篇论文,看一下第一篇,实在太长了(其实只有11页但相对于第二篇只有4页)。然后果断看第二篇,虽然已经是三十多年前的论文了。

  在这里费话两句,之前我因为《柔性字符串匹配》这本书说KMP不怎么实用,心里还感觉有点不爽,你说KMP不实用,那你拿一个实用且理论最坏复杂度为线性的算法让我看看。再加上找了一天多也只有一些BM算法的原始版。看了上面的第二篇文章,实现了一下其中的算法并在POJ上试了试,果然没问题。在这一刻我深深的折服于《柔性字符串匹配》了。这本书果然名副其实。值得推荐。

  好了,费话也就说这么多,接下来说点技术性的吧,看看Z. Galil. 是如何将BM的最坏情况复杂度降到线性的。在这里,我默认大家是明白BM算法原理的。加上上面已经有那么多文章是写BM算法的了(如果需要,建议阅读淘宝的那篇),我这里只讨论的优化了。

  

  以下讨论中,文本串用T表示,模式串用p表示。|S|表示字符串S的长度。默认情况下,n=|T|,m=|p|

 

  为什么原始的BM算法在最坏情况下是O(n*m)的呢?一个最简单的实例就是 p=a^m,T=a^n。就是说模式串是由m个a组成,文本是由n个a组成,在这种情况下,找出所有匹配复杂度就是O(n*m)的。造成这种情况最主要的就是BM算法在向后滑动的时候对之前匹配过的字符的利用率并不高,不像KMP,一旦匹配过,就不会再回过头再匹配一次,这也是我感觉KMP相当神的一点。
  在此之前先讨论几个概念,一个是字符串的周期性。
  直观的理解就是类似:abcabcabcabc,aabaabaab...这样的串,都是有周期性的。我们在这里认为这种模式串也具有周期性的——abcabcab,就是最一个重复单元是不完整的。这里的周期性指至少重复一次以上,这种串不算有周期性——abcdabc。随便提一下,这样的串不具有周期性——ababababc,这里的周期性是针对于整个字符串来说的。

  对于具用周期性的模式串,记模式串的最小正周期长度为ord。
  再引出一个定义:匹配块,T中的一段匹配块是一段连续的字符串,其中包含有匹配上的匹配串,并且这些匹配上的区域是相互重叠的并且尽可能向左右延伸。当然在匹配块中不包含多余的字符。举一个例子。(这里对匹配块的定义不准确,大家明白这个意思就行了。)
    p = aba
    T = abababaabaaba
  其中前面一部分就是一个匹配块:abababa,后面的abaaba不算,因为匹配上匹配串没有相互重叠。
  基于以下几点讨论,我们可以对BM算法进行优化。

    (1)如果文本串中不含模式串,那么BM算法在最坏情况下的比较次数为7n次。
    (2)BM算法的最坏情况复杂度为O(n+r*m),其中n为|T|,m为|p|,r为p在T中出现次数。
    (3)如果在上式中r>2n/m,则p是具有周期性的。
    (4)如果p不具周期性,那么BM算法在最坏情况下是线性的。
    (5)记一个匹配块中所有匹配上的位置为p0,p1,...,pk(以递增方式排列),
       那么对于0<i<=k,pi - p(i-1)=ord。而且每个匹配块中的匹配可以做到线性。
    (6)优化之后的BM' 算法是O(n)的。

  优化方法:每次成功匹配以后,向右移ord,并且在这种情况下只匹配那ord个字符。
  伪代码如下:

优化部分伪代码
 1     last = 0;
2 for(k=0; k+|p|<=|T|; )
3 {
4 for(i=pn-1; i>=last && T[k+i]==p[i]; )
5 i--;
6 if( i<last )
7 { /// 成功匹配
8 last = |p| - ord;
9 k += ord;
10 }
11 else
12 { /// 匹配失败
13 last = 0;
14 k += (BM算法中的移动距离);
15 }
16 }



  这里就是算法的核心了。这里 hust 有BM实现poj 3461的完整代码。
  这个优化只要知道的话就很好做了,在已有的BM代码上加两行就实现了。
  这个改进之后的BM' 算法在最坏情况下是线性的。
  下面是相关说明,论文上也有证明。这里以比较直白的方式对其进行一些分析。


(1)如果文本串中不含模式串,那么BM算法在最坏情况下的比较次数为7n次。
    这一条我也不知道怎么证明的,这里默认他是正确的吧。论文上说比较复杂没有介绍。

(2)BM算法的最坏情况复杂度为O(n+r*m),其中n为|T|,m为|p|,r为p在T中出现次数。
    这里我们把所有的r次完整匹配取出来,总共r*m次比较。这里我们想像用这r次匹配上的位置(取最左端)把T分割成了r+1个部分。对于前r个部分,我们还需要在其右端加上(m-1)字符。这里的r+1个文本串已经无法匹配上p了。所以这里可以直接使用(1)了。而这时的总长度约为(n+r*m),代入第一条可得总比较次数少于:
7*n + 8*r*m <- ( 7*(n+r*m) + r*m )

(3)如果在上式中r>2n/m,则p是具有周期性的。
    这里可以用鸽笼原理。先讨论当r>n/m时,即r*m>n。所有匹配的长度之和已经大于了|T|,这时,至少有两个匹配上的串是互相重叠的。这样,r>2*n/m就很好理解了吧,r*(m/2)>n,至少存在两次匹配上的串是互相重叠的,并且,重叠部分大于m/2。这代表了什么含意呢?想像一下,一个字符串,可以通过平移一小段距离,使重叠的那一部分互相匹配。这可以推出这个串是具有周期性的。

(4)如果p不具周期性,那么BM算法在最坏情况下也是线性的。
    这条可以用(3)的逆否命题得到:如果p不具有周期性,那么在上式中r<=2n/m。再代入(2)中表达式O(n+r*m)。可得此时BM算法复杂度O(3*n)即O(n)。

(5)记一个匹配块中所有匹配上的位置为p0,p1,...,pk(以递增方式排列)。那么对于0<i<=k,pi - p(i-1)=ord。
    这一点显然吧。其实我并不是特别明白论文中特意指出这一点的意义何在。

(6)BM' 算法是O(n)的。
    在这一点的理解上,很接近第二步的分析。把所有的匹配块提取出来,对于匹配块来说匹配是线性的,匹配块的总长是在O(n)级别的。这里需要说明一点的是匹配块是可以相互重叠的,但重叠部分不会大于m/2。而在剩下部分的匹配依然是O(n)级别的,所以总的时间复杂度也是O(n)的。

 

  最后加一点关于(4)点的讨论:我之前有一个错误的认识,我以为当p=b+a^m,T=a^n 时BM算法也是O(n*m)的,但我分析错了一个地方,我以为当每次匹配到p的最左边的b时,失败后只向右跳一个字符,而实际情况是向右跳了串长这么多。对BM的理解不够啊。

  附上poj测试图,虽然从这里看BM算法跑得比kmp慢了一点,但我已经很满足了。

            上面的两份代码是用BM实现的,下面两份是用KMP实现的。

 

posted on 2011-12-15 23:01  南柯一喵  阅读(12609)  评论(15编辑  收藏  举报