字符串与模式匹配算法(四):BM算法

一、BM算法介绍

  BM算法(Boyer-Moore算法)是罗伯特·波义尔(Robert Boyer)和杰·摩尔(J·Moore)在1977年共同提出的。与KMP算法不同的是,BM算法是模式串P由左向右移动,而字符的比较时由右向左进行。当文本字符与模式不匹配时,则根据预先定义好的“坏字符串偏移函数”和“好后缀偏移函数”计算出偏移量。

  坏字符偏移和好后缀偏移这两个函数胡的原理可以简单描述为:当比较工作进行到文中的j处时,新一轮的比较从右向左进行。不相匹配的情况出现在模式字符串的P[i]=a以及目标字符串的T[i+j]=b处,那么这时有T[i+j+1,...,j+m-1]=P[i+1,...,m-1]且P[i]≠T[i+j]。

  由好后缀偏移函数确定的偏移量分两种情况。首先,将模式字符串与目标字符串进行从右向左比较,当比较到字符P[i]时发现不同,则把T[i+j+1,……,j+m-1]=P[i+1],……,m-1]这一段看成u。如果在模式字符串里面u再次出现,且u的左侧是一个不同于a的前缀c时,则将模式字符串最右端再次出现的且前缀不是a的片段与文本串T[i+j+1,……,j+m-1]对齐,并将这次移动的偏移量存储在bmGs[i]中。

  其次,如果u在模式中没有再次出现,或者再次出现但u前面的字符是a,那么就将片段T[i+j+1,……,j+m-1]中最长后缀v与模式字符串前缀v对齐。若不存在这样的v,则模式串跳过u,并将这次移动的偏移量存储在bmGs[i]中。

  坏字符偏移函数的移动过程也分两种情况。首先,将目标字符串中的字符b同它在P[0,……,m-2]中从右向左首次出现的位置对齐并将这次移动的偏移量存储在bmBc[b]中。

  其次,如果在模式字符串中没有b,则将窗口的左端点与T[i+j+1]对齐,同理得到偏移量。

  无论是坏字符偏移,还是好后缀偏移函数,它们都在都是在为最后确定字符串的偏移做准备。经过以上步骤就可以最终确定模式字符串的具体偏移量,最终的右移量是时bmGs[i]和bmBc[b]-m+i+1两者中的大值。

二、坏字符启发式  

  这种方法称为坏字符启发法。 如果错误字符(即导致不匹配的文本符号)出现在模式中的其他位置,则也可以应用此功能。 然后可以移动模式,使其与该文本符号对齐。 下一个示例说明了这种情况。

Example:

0 1 2 3 4 5 6 7 8 9 ...
a b b a b a b a c b a
b a b a c            
    b a b a c        

  比较b-c导致不匹配。文本符号b出现在图案中的位置0和2处。可以移动该模式,以使模式中最右边的b与文本符号b对齐。

坏字符启发式预处理

  坏字符偏移是一个容量为σ的表,其中σ表示目标字符串中元素的种类数,每个字符对应的Ascall表中的十进制为下标。对于坏字符启发法,需要一个函数 badCharHeuristic,该函数会针对字母表中的每个符号产生其在模式中最右边出现的位置;如果在模式中未出现该符号,则得出-1。

  p = P∈A“P0 ... PM-1 模式的)和 a∈A 一个字母符号。然后 badCharHeuristic(p, a) = max{ j | pj = a },这里max(∅) = -1。

Example:

  • badCharHeuristic(text, x) = 2

  • badCharHeuristic(text, t) = 3

  字符串“文本”中符号“ x”最右出现在位置2。符号“ t”出现在位置0和3,最右边出现在位置3。

  某个模式p的出现函数存储在数组badchar中,该数组由字母符号索引。 对于每个符号A,相应的值badCharHeuristic(p,a)存储在badchar[a]中。

 1     /**
 2      * Boyer Moore的预处理功能,坏字符启发式
 3      * @param str 模式串
 4      * @param size 模式串的长度
 5      * @param badchar 记录字符最后一次出现在模式串中的位置
 6      */
 7     static void badCharHeuristic( char []str, int size,int badchar[])
 8     {
 9         int i;
10 
11         // 初始化所有字符出现为-1
12         for (i = 0; i < NO_OF_CHARS; i++)
13             badchar[i] = -1;
14 
15         // 填充每种字符最后出现的实际值
16         for (i = 0; i < size-1; i++)
17             badchar[(int) str[i]] = i;
18     }

三、好后缀启发式

  有时,坏字符启发法会失败。 在以下情况下,比较a-b会导致不匹配。 模式符号a的最右出现与文本符号a的对齐将产生负向偏移。 相反,可能会移位1。 但是,在这种情况下,最好从图案的结构中得出最大可能的移位距离。 此方法称为好后缀试探法。

Example:

0 1 2 3 4 5 6 7 8 9 ...
a b a a b a b a c b a
c a b a b            
    c a b a b        

  后缀ab已匹配。可以移动模式,直到模式中下一个出现的ab对准文本符号ab,即位置2。

  在以下情况下,后缀ab已匹配。模式中没有其他出现ab的情况,因此可以将模式移到ab后面,即移至位置5。

Example:

0 1 2 3 4 5 6 7 8 9 ...
a b c a b a b a c b a
c b a a b            
          c b a a b  

  在以下情况下,后缀bab已匹配。 模式中没有其他出现bab的情况。 但是在这种情况下,模式不能像以前一样移至位置5,而只能移至位置3,因为模式(ab)的前缀与bab的结尾匹配。 我们将此情况称为良好后缀启发式的情况2。

Example:

0 1 2 3 4 5 6 7 8 9 ...
a a b a b a b a c b a
a b b a b            
      a b b a b      

  模式由坏字符和后缀启发式方法给出的两个距离中的最长的移位。

后缀启发式的预处理

  对于后缀启发式,使用数组shift。 如果在位置i-1处发生不匹配,即如果从位置i开始的模式后缀已匹配,则每个条目shift [i]都包含模式的移位距离。 为了确定移动距离,必须考虑两种情况。

情况1:匹配的后缀出现在模式中的其他位置(图1)。

Figure 1: The matching suffix (gray) occurs somewhere else in the pattern

图1:匹配的后缀(灰色)出现在模式中的其他位置

情况2:只有部分匹配的后缀出现在模式的开头(图2)。

Figure 2: Only a part of the matching suffix occurs at the beginning of the pattern

图2:匹配后缀的仅一部分出现在模式的开头

情况1:

  这种情况类似于Knuth-Morris-Pratt预处理。 匹配的后缀是模式后缀的边界。 因此,必须确定模式后缀的边界。 但是,现在需要在给定边界和具有该边界的模式的最短后缀之间进行逆映射。

  而且,必须使边界不能由同一字符向左扩展,因为这会在模式串移位后引起另一个不匹配。

  在预处理算法的以下第一部分中,将计算数组bpos。 每个条目bpos [i]都包含模式后缀的最宽边界的起始位置,起始位置为i。 从位置m开始的后缀ε没有边界,因此bpos [m]设置为m + 1。

  与Knuth-Morris-Pratt(KMP)预处理算法相似,每个边界都是通过检查是否可以用同一字符向左扩展已知的较短边界来计算的。

  但是,边界不能向左扩展的情况也很有趣,因为如果发生不匹配,这会导致模式有希望的偏移。 因此,相应的移动距离保存在数组shift中,前提是该条目尚未被占用。 后者是较短后缀具有相同边框的情况。

 1 void preprocess_strong_suffix(int []shift, int []bpos,
 2                                          char []pat, int m)
 3     {
 4         int i = m, j = m + 1;
 5         bpos[i] = j;
 6 
 7         while(i > 0)
 8         {
 9             while(j <= m && pat[i - 1] != pat[j - 1])
10             {
11                 if (shift[j] == 0)
12                     shift[j] = j - i;
13 
14                 j = bpos[j];
15             }
16             i--; j--;
17             bpos[i] = j;
18         }
19     }

Example:

i: 0 1 2 3 4 5 6 7
p: a b b a b a b  
bpos: 5 6 4 5 6 7 7 8
shift: 0 0 0 0 2 0 4 1

  从位置2开始的后缀babab的最宽边界是bab,从位置4开始。因此,bpos [2] =4。从位置5开始的后缀ab的最宽边界是ε,从位置7开始。因此,bpos [5] = 7。

数组s的值由不能向左扩展的边界确定。

  从位置2开始的后缀babab具有从位置4开始的边界bab。由于p [1]≠p [3],因此该边界无法向左扩展。 差异4 – 2 = 2是bab匹配时的移位距离,然后发生不匹配。 因此,shift [4] = 2。

  从位置2开始的后缀babab也具有从位置6开始的边界b。此边界也不能扩展。 如果b匹配,则不匹配6 – 2 = 4是移位距离。 因此,shift [6] = 4。

  从位置6开始的后缀b具有从位置7开始的边界ε。该边界不能向左扩展。 如果没有匹配,即在第一次比较中发生不匹配,则7 – 6 = 1的差就是移动距离。 因此,shift [7] = 1。

  注:所谓边界就是模式P中从i开始的后缀中,其后缀中的前缀和后缀所匹配的最大公共子串开始的位置,也就是说 bpos[i] 到 m-1 的那一串 和 i开始往后的那段是一样的。ε是所有前后缀的匹配结果,相当于集合中的空集,没有边界。

情况2:

  在这种情况下,模式的匹配后缀的一部分出现在模式的开头。 这意味着该部分是模式的边界。 可以将模式移动到其最大匹配边界允许的范围内(图2)。

  在情况2的预处理中,对于每个后缀,确定该后缀中包含的模式的最宽边界。

  模式最宽边界的起始位置完全存储在bpos [0]中。 在上面的示例中,该值是5,因为边界ab从位置5开始。

  在以下预处理算法中,该值bpos [0]最初存储在数组shift 的所有空闲条目中。 但是,当模式的后缀变得比bpos [0]短时,算法会继续使用模式的下一个更宽的边界,即bpos [j]。

 1 void preprocess_case2(int []shift, int []bpos, int m)
 2 {
 3         int i, j;
 4         j = bpos[0];
 5         for(i = 0; i <= m; i++)
 6         {
 7             if(shift[i] == 0)
 8                 shift[i] = j;
 9             if (i == j)
10                 j = bpos[j];
11         }
12 }

Example:

i: 0 1 2 3 4 5 6 7
p: a b b a b a b  
bpos: 5 6 4 5 6 7 7 8
shift: 5 5 5 5 2 5 4 1

四、分析

  如果文本中模式的匹配项数量恒定,则在最坏的情况下,Boyer-Moore搜索算法会执行O(n)比较。 证明这一点相当困难。通常,Θ(n·m)比较是必要的,例如 如果模式是am,则文本是an。 通过稍微修改算法,即使在一般情况下,比较次数也可以限制为O(n)。如果字母表与模式的长度相比较大,则该算法将对平均值进行 O(n / m)比较。 这是因为由于坏字符启发法,通常有可能将m移位。

五、总结

  在遇到不匹配的情况下,Boyer-Moore算法使用两种不同的启发式方法来确定最大可能的移位距离:“坏字符”和“好后缀”启发式方法。 两种启发式方法都可以导致m的移位距离。 如果第一次比较导致不匹配,并且模式中根本没有出现相应的文本符号,那么对于坏字符启发式就是这种情况。 对于好后缀启发式法,如果只有第一个比较是匹配项,但该字符不在模式中的其他位置出现,则为这种情况。

  好后缀启发式算法的预处理相当难以理解和实现。 因此,有时会找到Boyer-Moore算法的版本,在该版本中,好后缀启发式法被遗弃了。 有观点认为,坏字符启发法就足够了,而好后缀启发法不会节省很多比较。 但是,对于字符数量小的字母串而言,情况并非如此。

参考:

String Matching - Boyer-Moore algorithm

猜你喜欢

转载自www.cnblogs.com/magic-sea/p/11846298.html