Boyer-Moore除了考虑Horspool算法(参考笔者的另一篇专门介绍Horspool算法的文章)的坏字符之外,还将模式串中已经匹配成功的后缀(叫做好后缀,good suffix)考虑进来,从而得到全部已经知道的启发信息(heuristic)。因此从理论上来说,BM算法应该是性能最佳的一个算法,实践中也证明了这一点。 这也是为什么BM算法经常用作精确匹配算法里面的性能测试基准算法。例如,在通过下面的图示就可以看出, KMP算法由于没有考虑进来bad character信息,比较次数比BM算法稍多:
(图一)
上面在i=4,j=4时出现mismatch,在KMP算法中的做法是找出j-1右边界位置的失败函数值作为下一个j值,这里f(j-1)=1, 因此j'=1(i值不变,即下次仍然将P[j']跟T[i]=C比较)。这里KMP算法的第二步只移动了3个字符位置。而在BM算法中的做法是首先找出bad character(=C)的位移值,这里C在pattern中未出现,用BM算法的case 1(参考前面另一篇文章中的BM简化版-Horspool算法的介绍),将模式串沿着文本向右移动m个位置,即向右移5;然后找出good suffix(长度等于0)的位移值,由于good suffix长度等于0,位移值为1。最后取good suffix shift和bad character shift的最大位移值,等于5,所以得出上面BM算法中第二步的位置。
******************************************************
BM算法也是分两大步骤:1)预处理计算出bad character shift表(这一步跟Horspool算法中的做法一模一样,详细情况参考笔者的另一篇文章)和good suffix shift表;2)匹配。
匹配过程比较简单,即取bad character shift和good suffix shift值的最大值即可。从逻辑上来说,因为两个位移值都保证了不会遗漏能够成功匹配的子串,如果取最大值就可以更大幅度地将模式串向右移,从而减少比较次数。
设文本T长度为n,模式串P长度为m。T的当前位置指针为i(0<=i<n),P的当前位置指针为j(0<=j<m)。设good suffix的长度为k(0<=k<m,即模式串中长度为k的后缀已经跟文本相应字符匹配成功),即好后缀为P[m-k...m-1]。好后缀位移值是goodSuffixShiftTable[k]。
如果单考虑good suffix位移,可以分为以下几种情况(另外参考Horspool算法那篇文章介绍的bad character的cases):
case 1) k=0,即模式串中最后一个字符P[m-1]不匹配,做法是将模式串沿文本向右移动一个字符,即goodSuffixShiftTable[0]=1。
例如图二中case 1,这里X表示下一次模式串采用good suffix的位移位置。如果采用bad character位移,就是Y位置,从图中可见Y的移动幅度更大,在BM算法中应该取Y的位置。
case 2) k>0,但是在模式串中可以找到一个最右边的子串(非后缀)跟good suffix - P[m-k...m-1]完全相等。做法是将模式串沿文本向右移动goodSuffixShiftTable[k]个字符。例如在图二中case 2中,当j=2时,T[i]=C, P[j]=A,k=3,可以找到最右边的非后缀子串P[1...3]跟后缀BAB完全相等。这时需要将模式串沿文本向右移动2个字符,即goodSuffixShiftTable[3]=2。
(图二)
case 3) k>0,但是不能找到像case 2中的最右边的非后缀子串跟好后缀(good suffix)完全相等。可以分成两个小cases:
case 3a) 模式串中没有任何前缀(prefix)跟模式串的good suffix的一个后缀(而且是一个proper后缀)完全相等,做法是将模式串向右移动m个字符,即goodSuffixShiftTable[k]=m,例如图二中case 3a,这里j=1,T[i]=a,P[j]=z,k=2,显然没有任何prefix跟bc的唯一一个proper后缀“c”完全相等,于是得出X为模式串的下一次匹配位置。
case 3b) 模式串中存在一个最长的前缀(prefix)跟模式串的good suffix的一个后缀(而且是一个proper后缀)完全相等,做法是将模式串向右移动goodSuffixShiftTable[k]个字符。例如图二中case 3b,这里j=2,T[i]=z,P[j]=c,k=3,存在最长的前缀ab跟好后缀的后缀ab完全相等。goodSuffixShiftTable[3]=4。
在Horspool算法的基础上,坏字符位移值(bad character shift)可以根据下面的公式得出:
badCharShift = j+1-min(j,1+q)
好后缀位移值就是goodSuffixShiftTable[k]。
下一个i指针位置向右移动量为m - j + goodSuffixShiftTable[k] - 1,即下一个i += m - j + goodSuffixShiftTable[k] - 1; 下一个j值永远是模式串尾部,即m-1,这跟Horspool算法是一样的。
*******************************************************
下面介绍一下如何计算good suffix位移表:
这里利用KMP算法中计算失败函数的做法来计算BM算法中的good suffix位移表,复杂度为O(m)。这样做的原因是KMP算法中的failure function从本质上等价于BM算法中的good suffix shift table,因为二者都是根据已经匹配成功的字符序列计算出的结果,只是形成序列的扫描方向相反。
首先将模式串转成逆序R,然后对逆序模式串计算出它的KMP失败函数f(j)。对f(j)从右往左循环一遍,计算上面case 2(即最右边子串等于长度为k的后缀)中的good suffix shift值,循环结束时有这样的结果:
1)还有一部分k(k>=0)对应的good suffix shift值为0(即数组初始值),这时对应case 1,case 3a和case 3b(见下面的说明)。
2)f(j)>0,如果这样的f(j)有多个,循环最后的那个f(j)就相当于原始模式串最右边的子串长度(该子串等于原始模式串中长度为k=f(j)的后缀),所以这个f(j)就是k值。位移值采用如下公式计算:
(m - f[j]) - (m-j-1)
接下来设置goodSuffixShiftTable[0]=1,这是k=0(即case 1)时的一个特定值。
最后考虑某些k的good suffix shift仍然为0的情况。因为f(m-1)就是case 3中匹配到的最长前缀。见下图中蓝色部分区域。如果f(m-1)=0,就是case 3a,位移值设置为m。
如果f(m-1)>0,对应case 3b,这时goodSuffixShiftTable[k]应修正为m-f(m-1),即将模式串首字符跟长度为f(m-1)的后缀的首字符对齐。需要说明的是,在这种情况下(即f(m-1)>0), 这样的k必定满足条件k>f(m-1),用反证法证明:到目前为止goodSuffixShiftTable[k]=0的那些k值如果满足0<k<=f(m-1)的话,在上面2)中得到的结果会有goodSuffixShiftTable[k]>0, 理由是k<=f(m-1)说明一定存在一个长度为k子串与某一个长度为k的后缀完全相等,即满足case 2,显然对于case 2的情况必有goodSuffixShiftTable[k]>0,与goodSuffixShiftTable[k]=0前提矛盾。这也是为什么下面实现代码中用"if(goodSuffixShift[k]==0)"代替"if(goodSuffixShift[k]==0 && prefixMaxLen<k)"的原因。
下图中的S就是good suffix shift值列表。
(图三)
实现:
测试输出: