文章目录
- [1. 最好情况](#1. 最好情况)
- [2. 最坏情况](#2. 最坏情况)
1. 最好情况
以下,就让我们从最好情况的角度,来考察坏字符策略的性能。实际上,在最好情况下的性能之好,要远远超过我们的想象。具体来说,此时的时间成本可以度量为 O(n/m)。没错,除法。你能构造出这样的一个具体实例吗?这就是一个:
可以看到,模式串完全由0组成,而文本串呢?则由与模式串等长的若干个片段依次拼接而成。在每一个片段中,末字符都不是0,比如说取作1,而其余的字符实质上都无所谓,因此笼统的记作 x。我不妨就此例来走一遍 BM 算法。
- 第一次对齐的位置在这里,而首次比对就是0/1的失配。在这里,0就是坏字符,而且在模式串中,根本就找不到足以与 1 匹配的字符。
- 于是,BM法将会把模式串整体的移过这个失配的位置,并使之与文本串中的下一个片段彼此对齐。而接下来的故事,与刚才的那个片段完全一样。首先末字布 0 无法与 1 匹配,而且其余的 0 也不能与之匹配。
- 因此,算法将再次整体的后移模式串。以致再次、再再次、持续地整体移动下去。
纵观整个过程,每经过常数次的比对,我们就可以向后整体的移动 m 个字符。既然文本串累计不过 n 个字符,因此在这种情况下,算法至多会移动 n/m 次。
由此可见,在这种情况下,算法的确只需要运行这么多的时间,即便是相对于 KMP 而言,这也是一个极大的提高,更不用说蛮力算法了。这个例子并不失一般性,实际上只要 P 中的字符都是坏字符,那么每次我们都可以整体地对它进行移动,也就是说我们只需要常数次的比较,就可以整体地排除掉 m 的对齐位置。
由此可见,如果说 KMP 算法是个利用经验的高手,那么 BM 算法则是非常善于借鉴教训的高手。从这个意义上讲,BM 算法更加欢迎失败比对的出现。那么在什么情况下更容易出现失败的比对呢?
也就是说,对于任何一对随机出现的字符所做的比对,失配的概率在什么情况下更小呢?在这里我们再次回到那样一个重要的指标,也就是字母表的规模。实际上字母表的规模越大,单次匹配成功的概率也就越小,失配的概率也就越大,从而 BM 算法的优势也就更为明显。
比如在处理汉字甚至 unicode 编码时,BM算法就是再适合不过的了。
2. 最坏情况
然而另一方面很遗憾,BM算法在最差情况下的性能也的确非常差。
具体来说,它的算法复杂度有可能会退化到蛮力算法的水平,也就是 O(n*m)。就此,你能举出一个具体的实例吗?我可以给你一点提示,你不妨去参考一下蛮力算法最坏情况的那个实例。
是的,我想你已经想到了,就是这个。这一次文本串倒是完全由0组成的,而模式串也几乎是由 0 组成的,唯一的例外是它的首字符。按照 BM 算法的流程,我们首先需要从末字符开始进行比对,而且我们会经历一系列的成功,直到最后一步才失败。可以看到,在这次失败之前,我们已经花费了 O(m) 的成本。
然而最糟糕的还不是这个,因为我们花费了如此之高的成本所换来的那个教训,居然对我们不会有太多的帮助。因为此时正属于我们此前所讲的那种最为特殊的情况,也就是说坏字符的替代者应该是 0。但是在模式上中,最后出现的那个 0 过于靠后,以至于如果我们需要将它与此前的适配字符对齐,将导致模式串的左移而不是右移。因此在这种情况下,我们只好搬出那个假想的通配哨兵,并用那通配的哨兵与这个失配的字符相对齐。
非常可惜,这样的效果只相当于模式串向右移动一个单位。没错,我们每花费 O(m) 的成本,换来的收获只是向后移动了一步,而总体共有 n。因此在这种情况下的总体计算成本的确应该是 n 与 m 的乘积。
我们不妨来反思一下, BM 算法当前的这个版本为何还有可能会出现如此之差的情况呢?没错,经验。我们已经看到 BM 算法目前的这个版本的确已经能够很好地借鉴教训。也正因为此,它才能够在最好情况下有出色的表现。然而,很遗憾,到目前为止,它还没能够有效地利用好经验。具体来说,也就是此前的那些成功比对所提供的有益信息。实际上,完整的 BM 算法的确也可以同时很好地利用这方面的信息。
因此,我们需要给 BM 算法增加一个策略,也就是所谓的好后缀策略。这个策略再加上我们刚刚介绍的坏字符策略,就可以使得 BM 算法变得几乎完美。