grep之字符串搜索算法Boyer-Moore由浅入深(比KMP快3-5倍)

1. 简单介绍

在用于查找子字符串的算法当中,BM(Boyer-Moore)算法是目前被认为最高效的字符串搜索算法,它由Bob Boyer和J Strother Moore设计于1977年。 通常状况下,比KMP算法快3-5倍。该算法经常使用于文本编辑器中的搜索匹配功能,好比你们所熟知的GNU grep命令使用的就是该算法,这也是GNU grep比BSD grep快的一个重要缘由,具体推荐看下我最近的一篇译文“为何GNU grep如此之快?”做者是GNU grep的编写者Mike Haertel。算法

 

2. 主要特征

假设文本串text长度为n,模式串pattern长度为m,BM算法的主要特征为:数组

  • 从右往左进行比较匹配(通常的字符串搜索算法如KMP都是从从左往右进行匹配);框架

  • 算法分为两个阶段:预处理阶段和搜索阶段;编辑器

  • 预处理阶段时间和空间复杂度都是是O(m+sigma),sigma是字符集大小,通常为256;性能

  • 搜索阶段时间复杂度是O(mn);spa

  • 当模式串是非周期性的,在最坏的状况下算法须要进行3n次字符比较操做;设计

  • 算法在最好的状况下达到O(n / m),好比在文本串bn中搜索模式串am-1b ,只须要n/m次比较。code

这些特征先让你们对该算法有个基本的了解,等看懂了算法再来看这些特征又会有些额外的收获。cdn

 

3.算法基本思想

常规的匹配算法移动模式串的时候是从左到右,而进行比较的时候也是从左到右的,基本框架是:ci

1
2
3
4
5
6
7
8
9
10
while (j <= strlen (text) - strlen (pattern)){
     for (i = 0; i < strlen (pattern) && pattern[i] == text[i + j]; ++i);
 
     if (i == strlen (pattern)) {
         Match;
         break ;
     }
     else
         ++j;
}

而BM算法在移动模式串的时候是从左到右,而进行比较的时候是从右到左的,基本框架是:

1
2
3
4
5
6
7
8
9
10
while (j <= strlen (text) - strlen (pattern)){
     for (i = strlen (pattern); i >= 0 && pattern[i] == text[i + j]; --i);
 
     if (i < 0)) {
         Match;
         break ;
     }
     else
         j += BM();
}

BM算法的精华就在于BM(text, pattern),也就是BM算法当不匹配的时候一次性能够跳过不止一个字符。即它不须要对被搜索的字符串中的字符进行逐一比较,而会跳过其中某些部分。一般搜索关键字越长,算法速度越快。它的效率来自于这样的事实:对于每一次失败的匹配尝试,算法都可以使用这些信息来排除尽量多的没法匹配的位置。即它充分利用待搜索字符串的一些特征,加快了搜索的步骤。

BM算法实际上包含两个并行的算法(也就是两个启发策略):坏字符算法(bad-character shift)和好后缀算法(good-suffix shift)。这两种算法的目的就是让模式串每次向右移动尽量大的距离(即上面的BM()尽量大)。

下面不直接书面解释这两个算法,为了更加通俗易懂,先用实例说明吧,这是最容易接受的方式。

 

4. 字符串搜索头脑风暴

你们来头脑风暴下:如何加快字符串搜索?举个很简单的例子,以下图所示,navie表示通常作法,逐个进行比对,从右向左,最后一个字符c与text中的d不匹配,pattern右移一位。但你们看一下这个d有什么特征?pattern中没有d,所以你无论右移一、二、三、4位确定仍是不匹配,何须花这个功夫呢?直接右移5(strlen(pattern))位再进行比对不是更好吗?好,就这样作,右移5位后,text中的b与pattern中的c比较,发现仍是不一样,这时咋办?b在pattern中有因此不能一下右移5位了,难道直接右移一位吗?No,能够直接将pattern中的b右移到text中b的位置进行比对,可是pattern中有两个b,右移哪一个b呢?保险的办法是用最右边的b与text进行比对,为啥?下图说的很清楚了,用最左边的b太激进了,容易漏掉真正的匹配,图中用最右边的b后发现正好全部的都匹配成功了,若是用最左边的不就错过了这个匹配项吗?这个启发式搜索就是BM算法作的。

BM-math

 

But, 若是遇到下面这样的状况,开始pattern中的c和text中的b不匹配,Ok,按上面的规则将pattern右移直至最右边的b与text的b对齐进行比对。再将pattern中的c与text中的c进行比对,匹配继续往左比对,直到位置3处pattern中的a与text中的b不匹配了,按上面讲的启发式规则应该将pattern中最右边的b与text的b对齐,可这时发现啥了?pattern走了回头路,干嘛?固然不干,才不要那么傻,针对这种状况,只须要将pattern简单的右移一步便可,坚持不走回头路!

BM-math02

好了,这就是所谓的“坏字符算法”,简单吧,通俗易懂吧,上面用红色粗体字标注出来的b就是“坏字符”,即不匹配的字符,坏字符是针对text的。

BM难道就这么简单?就一个启发式规则就搞定了?固然不是了,你们再次头脑风暴一下,有没有其余加快字符串搜索的方法呢?好比下面的例子

BM-math03

一开始利用了坏字符算法一下移了4位,不错,接下来遇到了回头路,没办法只能保守移一位,但真的就只能移一位吗?No,由于pattern中前面其余位置也有刚刚匹配成功的后缀ab,那么将pattern前面的ab右移到text刚匹配成功的ab对齐继续往前匹配不是更好吗?这样就能够一次性右移两位了,很好的有一个启发式搜索规则啊。有人可能想:要是前面没已经匹配成功的后缀咋办?是否是就无效了?不彻底是,这要看状况了,好比下面这个例子。

BM-math04

 

cbab这个后缀已经成功匹配,而后b没成功,而pattern前面也没发现cbab这样的串,这样就直接保守移一位?No,前面有ab啊,这是cbab后缀的一部分,也能够好好利用,直接将pattern前面的ab右移到text已经匹配成功的ab位置处继续往前匹配,这样一会儿就右移了四位,很好。固然,若是前面彻底没已经匹配成功的后缀或部分后缀,好比最前面的babac,那就真的不能利用了。

好了,这就是所谓的“好后缀算法”,简单吧,通俗易懂吧,上面用红色字标注出来的ab(前面例子)和cbab(上面例子)就是“好后缀”,好后缀是针对pattern的。

下面,最后再举个例子说明啥是坏字符,啥是好后缀。

主串  :  mahtavaatalomaisema omalomailuun

模式串: maisemaomaloma

坏字符:主串中的“t”为坏字符。

好后缀:模式串中的aloma为“好后缀”。

BM就这么简单?是的,容易理解但并非每一个人都能想到的两个启发式搜索规则就造就了BM这样一个优秀的算法。那么又有个问题?这两个算法怎么运用,一下坏字符的,一下好后缀的,何时该用坏字符?何时该用好后缀呢?很好的问题,这就要看哪一个右移的位数多了,好比上面的例子,一开始若是用好后缀的话只能移一位而用坏字符就能右移三位,此时固然选择坏字符算法了。接下来若是继续用坏字符则只能右移一位而用好后缀就能一下右移四位,这时候你说用啥呢?So,这两个算法是“并行”的,哪一个大用哪一个。

光用例子说明固然不够,太浅了,并且还不必定能彻底覆盖全部状况,不精确。下面就开始真正的理论探讨了。

 

5. BM算法理论探讨

(1)坏字符算法

当出现一个坏字符时, BM算法向右移动模式串, 让模式串中最靠右的对应字符与坏字符相对,而后继续匹配。坏字符算法有两种状况。

Case1:模式串中有对应的坏字符时,让模式串中最靠右的对应字符与坏字符相对(PS:BM不可能走回头路,由于如果回头路,则移动距离就是负数了,确定不是最大移动步数了),以下图。

BM-math05

Case2:模式串中不存在坏字符,很好,直接右移整个模式串长度这么大步数,以下图。

BM-math06

 

(2)好后缀算法

若是程序匹配了一个好后缀, 而且在模式中还有另一个相同的后缀或后缀的部分, 那把下一个后缀或部分移动到当先后缀位置。假如说,pattern的后u个字符和text都已经匹配了,可是接下来的一个字符不匹配,我须要移动才能匹配。若是说后u个字符在pattern其余位置也出现过或部分出现,咱们将pattern右移到前面的u个字符或部分和最后的u个字符或部分相同,若是说后u个字符在pattern其余位置彻底没有出现,很好,直接右移整个pattern。这样,好后缀算法有三种状况,以下图所示:

Case1:模式串中有子串和好后缀彻底匹配,则将最靠右的那个子串移动到好后缀的位置继续进行匹配。

BM-math07

Case2:若是不存在和好后缀彻底匹配的子串,则在好后缀中找到具备以下特征的最长子串,使得P[m-s…m]=P[0…s]。

BM-math08

Case3:若是彻底不存在和好后缀匹配的子串,则右移整个模式串。

(3)移动规则

BM算法的移动规则是:

将3中算法基本框架中的j += BM(),换成j += MAX(shift(好后缀),shift(坏字符)),即

BM算法是每次向右移动模式串的距离是,按照好后缀算法和坏字符算法计算获得的最大值。

shift(好后缀)和shift(坏字符)经过模式串的预处理数组的简单计算获得。坏字符算法的预处理数组是bmBc[],好后缀算法的预处理数组是bmGs[]。

 

6. BM算法具体执行

BM算法子串比较失配时,按坏字符算法计算pattern须要右移的距离,要借助bmBc数组,而按好后缀算法计算pattern右移的距离则要借助bmGs数组。下面讲下怎么计算bmBc[]和bmGs[]这两个预处理数组。

(1)计算坏字符数组bmBc[]

这个计算应该很容易,彷佛只须要bmBc[i] = m – 1 – i就好了,但这样是不对的,由于i位置处的字符可能在pattern中多处出现(以下图所示),而咱们须要的是最右边的位置,这样就须要每次循环判断了,很是麻烦,性能差。这里有个小技巧,就是使用字符做为下标而不是位置数字做为下标。这样只须要遍历一遍便可,这貌似是空间换时间的作法,但若是是纯8位字符也只须要256个空间大小,并且对于大模式,可能自己长度就超过了256,因此这样作是值得的(这也是为何数据越大,BM算法越高效的缘由之一)。

BM-math09

如前所述,bmBc[]的计算分两种状况,与前一一对应。

Case1:字符在模式串中有出现,bmBc['v']表示字符v在模式串中最后一次出现的位置,距离模式串串尾的长度,如上图所示。

Case2:字符在模式串中没有出现,如模式串中没有字符v,则BmBc['v'] = strlen(pattern)。

写成代码也很是简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void PreBmBc( char *pattern, int m, int bmBc[])
{
     int i;
 
     for (i = 0; i < 256; i++)
     {
         bmBc[i] = m;
     }
 
     for (i = 0; i < m - 1; i++)
     {
         bmBc[pattern[i]] = m - 1 - i;
     }
}

计算pattern须要右移的距离,要借助bmBc数组,那么bmBc的值是否是就是pattern实际要右移的距离呢?No,想一想也不是,好比前面举例说到利用bmBc算法还可能走回头路,也就是右移的距离是负数,而bmBc的值绝对不多是负数,因此二者不相等。那么pattern实际右移的距离怎么算呢?这个就要看text中坏字符的位置了,前面说过坏字符算法是针对text的,仍是看图吧,一目了然。图中v是text中的坏字符(对应位置i+j),在pattern中对应不匹配的位置为i,那么pattern实际要右移的距离就是:bmBc['v'] – m + 1 + i。

BM-math10

(2)计算好后缀数组bmGs[]

这里bmGs[]的下标是数字而不是字符了,表示字符在pattern中位置。

如前所述,bmGs数组的计算分三种状况,与前一一对应。假设图中好后缀长度用数组suff[]表示。

Case1:对应好后缀算法case1,以下图,j是好后缀以前的那个位置。

BM-math11

Case2:对应好后缀算法case2:以下图所示:

BM-math13

Case3:对应与好后缀算法case3,bmGs[i] = strlen(pattern)= m

BM-math14

这样就更加清晰了,代码编写也比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void PreBmGs( char *pattern, int m, int bmGs[])
{
     int i, j;
     int suff[SIZE]; 
 
     // 计算后缀数组
     suffix(pattern, m, suff);
 
     // 先所有赋值为m,包含Case3
     for (i = 0; i < m; i++)
     {
         bmGs[i] = m;
     }
 
     // Case2
     j = 0;
     for (i = m - 1; i >= 0; i--)
     {
         if (suff[i] == i + 1)
         {
             for (; j < m - 1 - i; j++)
             {
                 if (bmGs[j] == m)
                     bmGs[j] = m - 1 - i;
             }
         }
     }
 
     // Case1
     for (i = 0; i <= m - 2; i++)
     {
         bmGs[m - 1 - suff[i]] = m - 1 - i;
     }
}

So easy? 结束了吗?还差一步呢,这里的suff[]咋求呢?

在计算bmGc数组时,为提升效率,先计算辅助数组suff[]表示好后缀的长度。

suff数组的定义:m是pattern的长度

a. suffix[m-1] = m;

b. suffix[i] = k

    for [ pattern[i-k+1] ….,pattern[i]] == [pattern[m-1-k+1],pattern[m-1]]

看上去有些晦涩难懂,实际上suff[i]就是求pattern中以i位置字符为后缀和以最后一个字符为后缀的公共后缀串的长度。不知道这样说清楚了没有,仍是举个例子吧:

i     : 0 1 2 3 4 5 6 7
pattern: b c  a b a b a b

当i=7时,按定义suff[7] = strlen(pattern) = 8

当i=6时,以pattern[6]为后缀的后缀串为bcababa,以最后一个字符b为后缀的后缀串为bcababab,二者没有公共后缀串,因此suff[6] = 0

当i=5时,以pattern[5]为后缀的后缀串为bcabab,以最后一个字符b为后缀的后缀串为bcababab,二者的公共后缀串为abab,因此suff[5] = 4

以此类推……

当i=0时,以pattern[0]为后缀的后缀串为b,以最后一个字符b为后缀的后缀串为bcababab,二者的公共后缀串为b,因此suff[0] = 1

这样看来代码也很好写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void suffix( char *pattern, int m, int suff[])
{
     int i, j;
     int k;
 
     suff[m - 1] = m;
 
     for (i = m - 2; i >= 0; i--)
     {
         j = i;
         while (j >= 0 && pattern[j] == pattern[m - 1 - i + j]) j--;
 
         suff[i] = i - j;
     }
}

这样可能就万事大吉了,但是总有人对这个算法不满意,感受太暴力了,因而有聪明人想出一种方法,对上述常规方法进行改进。基本的扫描都是从右向左,改进的地方就是利用了已经计算获得的suff[]值,计算如今正在计算的suff[]值。具体怎么利用,看下图:

i是当前正准备计算suff[]值的那个位置。

f是上一个成功进行匹配的起始位置(不是每一个位置都能进行成功匹配的,  实际上可以进行成功匹配的位置并很少)。

g是上一次进行成功匹配的失配位置。

若是i在g和f之间,那么必定有P[i]=P[m-1-f+i];而且若是suff[m-1-f+i] < i-g, 则suff[i] = suff[m-1-f+i],这不就利用了前面的suff了吗。

BM-math15

PS:这里有些人可能以为应该是suff[m-1-f+i] <= i – g,由于若suff[m-1-f+i] = i – g,仍是没超过suff[f]的范围,依然能够利用前面的suff[],但这是错误的,好比一个极端的例子:

i      :0 1 2 3 4 5 6 7 8 9
pattern:a  a a a a b a a a  a

suff[4] = 4,这里f=4,g=0,当i=3是,这时suff[m-1=f+i]=suff[8]=3,而suff[3]=4,二者不相等,由于上一次的失配位置g可能会在此次获得匹配。

好了,这样解释事后,代码也比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void suffix( char *pattern, int m, int suff[]) {
    int f, g, i;
 
    suff[m - 1] = m;
    g = m - 1;
    for (i = m - 2; i >= 0; --i) {
       if (i > g && suff[i + m - 1 - f] < i - g)
          suff[i] = suff[i + m - 1 - f];
       else {
          if (i < g)
             g = i;
          f = i;
          while (g >= 0 && pattern[g] == pattern[g + m - 1 - f])
             --g;
          suff[i] = f - g;
       }
    }
}

结束了?OK,能够说重要的算法都完成了,但愿你们可以看懂,为了验证你们到底有没有彻底看明白,下面出个简单的例子,你们算一下bmBc[]、suff[]和bmGs[]吧。

举例以下:

BM-math16

 PS:这里也许有人会问:bmBc['b']怎么等于2,它不是最后出如今pattern最后一个位置吗?按定义应该是0啊。请你们仔细看下bmBc的算法:

1
2
3
4
for (i = 0; i < m - 1; i++)
   {
       bmBc[pattern[i]] = m - 1 - i;
   }

这里是i < m – 1不是i < m,也就是最后一个字符若是没有在前面出现过,那么它的bmBc值为m。为何最后一位不计算在bmBc中呢?很容易想啊,若是记在内该字符的bmBc就是0,按前所述,pattern须要右移的距离bmBc['v']-m+1+i=-m+1+i <= 0,也就是原地不动或走回头路,固然不干了,前面这种状况已经说的很清楚了,因此这里是m-1。

好了,全部的终于都讲完了,下面整合一下这些算法吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include <stdio.h>
#include <string.h>
 
#define MAX_CHAR 256
#define SIZE 256
#define MAX(x, y) (x) > (y) ? (x) : (y)
 
void BoyerMoore( char *pattern, int m, char *text, int n);
 
int main()
{
     char text[256], pattern[256];
 
     while (1)
     {
         scanf ( "%s%s" , text, pattern);
         if (text == 0 || pattern == 0) break ;
 
         BoyerMoore(pattern, strlen (pattern), text, strlen (text));
         printf ( "\n" );
     }
 
     return 0;
}
 
void print( int *array, int n, char *arrayName)
{
     int i;
     printf ( "%s: " , arrayName);
     for (i = 0; i < n; i++)
     {
         printf ( "%d " , array[i]);
     }
     printf ( "\n" );
}
 
void PreBmBc( char *pattern, int m, int bmBc[])
{
     int i;
 
     for (i = 0; i < MAX_CHAR; i++)
     {
         bmBc[i] = m;
     }
 
     for (i = 0; i < m - 1; i++)
     {
         bmBc[pattern[i]] = m - 1 - i;
     }
 
/*  printf("bmBc[]: ");
     for(i = 0; i < m; i++)
     {
         printf("%d ", bmBc[pattern[i]]);
     }
     printf("\n"); */
}
 
void suffix_old( char *pattern, int m, int suff[])
{
     int i, j;
 
     suff[m - 1] = m;
 
     for (i = m - 2; i >= 0; i--)
     {
         j = i;
         while (j >= 0 && pattern[j] == pattern[m - 1 - i + j]) j--;
 
         suff[i] = i - j;
     }
}
 
void suffix( char *pattern, int m, int suff[]) {
    int f, g, i;
 
    suff[m - 1] = m;
    g = m - 1;
    for (i = m - 2; i >= 0; --i) {
       if (i > g && suff[i + m - 1 - f] < i - g)
          suff[i] = suff[i + m - 1 - f];
       else {
          if (i < g)
             g = i;
          f = i;
          while (g >= 0 && pattern[g] == pattern[g + m - 1 - f])
             --g;
          suff[i] = f - g;
       }
    }
 
//   print(suff, m, "suff[]");
}
 
void PreBmGs( char *pattern, int m, int bmGs[])
{
     int i, j;
     int suff[SIZE]; 
 
     // 计算后缀数组
     suffix(pattern, m, suff);
 
     // 先所有赋值为m,包含Case3
     for (i = 0; i < m; i++)
     {
         bmGs[i] = m;
     }
 
     // Case2
     j = 0;
     for (i = m - 1; i >= 0; i--)
     {
         if (suff[i] == i + 1)
         {
             for (; j < m - 1 - i; j++)
             {
                 if (bmGs[j] == m)
                     bmGs[j] = m - 1 - i;
             }
         }
     }
 
     // Case1
     for (i = 0; i <= m - 2; i++)
     {
         bmGs[m - 1 - suff[i]] = m - 1 - i;
     }
 
//  print(bmGs, m, "bmGs[]");
}
 
void BoyerMoore( char *pattern, int m, char *text, int n)
{
     int i, j, bmBc[MAX_CHAR], bmGs[SIZE];
 
     // Preprocessing
     PreBmBc(pattern, m, bmBc);
     PreBmGs(pattern, m, bmGs);
 
     // Searching
     j = 0;
     while (j <= n - m)
     {
         for (i = m - 1; i >= 0 && pattern[i] == text[i + j]; i--);
         if (i < 0)
         {
             printf ( "Find it, the position is %d\n" , j);
             j += bmGs[0];
             return ;
         }
         else
         {
             j += MAX(bmBc[text[i + j]] - m + 1 + i, bmGs[i]);
         }
     }
 
     printf ( "No find.\n" );
}

运行效果以下:

BM-math17

相关文章
相关标签/搜索