再谈KMP/BM算法(II)
KMP算法中next[i]与Zi(S)的对应关系
我在《KMP算法详解》一文中已经介绍了next[i]的含义,对于S[i],next[i]的意义是,如果存在k使得S[1...i-k]=S[k...i-1]且S[i-k+1]!=S[i],那么next[i]=i-k+1。实际上对于满足条件的k,其Z值Zk(S)就满足k+Zk(S)=i,next[i]=Zk(S)+1,所以我们可以用如下方法根据模式串S的Zi(S)表填写对应的next[i]表。
规则一,从头到尾遍历Zi(S),当遍历到元素k时,如果Zk(S)!=0,那么next[k+Zk(S)]=Zk(S)+1,如果还存在k'使得k+Zk(S)=k'+Zk'(S)那么next[k+Zk(S)]等于Zk(S)+1与Zk'(S)+1的较大者。
规则二,对于遍历Zi(S)列表之后,尚未填写的元素的next值,我们按照如下原则填充,对于元素S[i],如果S[i]=S[1],则其next值next[i]=0,否则next[i]=1。
根据上面的原则,我们对于《KMP算法详解》中的老例子通过Zi(S)构建next[i]的表格如下。这里对于S[8],由于4+Z4(S)=8,7+Z7(S)=8,所以我们选择其中的较大者Z4(S)=4,令next[8]=Z4(S)+1=5。对于S[9],由于9+Z9(S)超出了pattern数组的范围,所以我们不使用该Z值计算next跳转表。实际对于下表,除了next[8]之外,其余均是由规则二填写。相较于KMP三人给出的next表填写算法,利用Z值表填写next表固然增加了一个转换层,降低了算法效率,但是从易理解的角度讲,由Z值到next值的转换是十分有意义的。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
pattern | a | b | c | a | b | c | a | c | a | b |
Zi(S) | 0 | 0 | 0 | 4 | 0 | 0 | 1 | 0 | 2 | 0 |
next | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 5 | 0 | 1 |
BM算法goodsuffix[i]与Zi(S)的对应关系
用Z值表填写goodsuffix表的过程,要比填写next表复杂得多。首先,BM算法使用的是后缀自包含而Z值计算的是前缀,另一方面我们还需要找到最长的与后缀相匹配的前缀的长度,来修正跳转值。这里我们分别来处理这两个问题。
对于BM算法中的模式串S,我们可以计算其逆串Sr的Z值,Zi(Sr)。例如,对于S="abcxxxabc",Sr="cbaxxxcba",我们可以得到Sr的Z值表如下图所示
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
Sr | c | b | a | x | x | x | c | b | a |
Zi(Sr) | 0 | 0 | 0 | 0 | 0 | 0 | 3 | 0 | 0 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
S | a | b | c | x | x | x | a | b | c |
rpr(i) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
之后,我们可以计算出未修正的好后缀跳转表。对于rpr(i)=0的元素,goodsuffix'[i]=patlen+n-i,对于rpr(i)!=0的元素,goodsuffix'[i]=n-rpr(i),其中n是模式串最末元素的索引值。如果模式串的首字符从0开始的话,n!=patlen这里要特别注意。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
S | a | b | c | x | x | x | a | b | c |
goodsuffix' | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 1 |
另外,我们还需要找到与后缀匹配的最长前缀p,用于修正goodsuffix'的跳转步数。p值在构建Zi(Sr)的时候可以得到,对于Sr中的元素Sr[i],如果有i+Zi(Sr)-1=n,那么p=Zi(Sr),如果有多个i满足该条件,则p等于其中的最大者。上例中对于Sr="cbaxxxcba",我们有7+Z7(Sr)-1=9,所以p=Z7(Sr)=3。在修正goodsuffix'的跳转步数时,我们对于n-i>=p的元素goodsuffix'值统一减去p即可得到最终的goodsuffix值。如下图
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
S | a | b | c | x | x | x | a | b | c |
goodsuffix | 14 | 13 | 12 | 11 | 10 | 9 | 11 | 10 | 1 |
上面虽然列出了4个步骤,但是在实际计算BM的好后缀跳转表的过程中,除了Zi(Sr)需要单独计算之外,其余三个步骤,可以一次完成。
在使用Z值表计算KMP算法或者是BM算法的跳转表的过程中,模式串起始索引要特别注意,如果S[0...n]从0开始,则要对一些地方做修正。因为如果模式串从索引0的位置开始,其最末元素n!=patlen,所以在计算过程中,哪里用的是模式串的长度,哪里用的是模式串最末元素的索引,要格外留心。