引言
KMP它并不是效率最高的算法,实际采用并不多。各种文本编辑器的”查找”功能(Ctrl+F),大多采用Boyer-Moore算法。
Boyer-Moore算法不仅效率高,而且构思巧妙,容易理解。1977年,德克萨斯大学的Robert S
. Boyer
教授和J Strother Moore
教授发明了这种算法。
在用于查找子字符串的算法当中,BM(Boyer-Moore)算法是目前被认为最高效的字符串搜索算法。 一般情况下,比KMP算法快3-5倍。该算法常用于文本编辑器中的搜索匹配功能,比如大家所熟知的GNU grep
命令使用的就是该算法,这也是GNU grep
比BSD grep
快的一个重要原因。
概念
Moore教授例子
根据Moore
教授自己的例子来解释这种算法。
假定字符串(文本串)为”HERE IS A SIMPLE EXAMPLE”,搜索词(模式串)为”EXAMPLE”。
首先,”字符串”与”搜索词”头部对齐,从尾部开始比较。
这是一个很聪明的想法,因为如果尾部字符不匹配,那么只要一次比较,就可以知道前7个字符肯定不是要找的结果。
我们看到,”S”与”E”不匹配。这时,”S”就被称为”坏字符”(bad character),即不匹配的字符。我们还发现,”S”不包含在搜索词”EXAMPLE”之中,这意味着可以把搜索词直接移到”S”的后一位。
依然从尾部开始比较,发现”P”与”E”不匹配,所以”P”是”坏字符”。但是,”P”包含在搜索词”EXAMPLE”之中。所以,将搜索词后移两位,两个”P”对齐。
我们由此总结出”坏字符规则”:1
后移位数 = 坏字符的位置(在搜索串中的位置) - 搜索词中的上一次出现位置(最新比较的位置)
如果”坏字符”不包含在搜索词之中,则上一次出现位置为 -1。
以”P”为例,它作为”坏字符”,出现在搜索词的第6位(从0开始编号),在搜索词中的上一次出现位置为4,所以后移6 - 4 = 2
位。再以前面第二步的”S”为例,它出现在第6位,上一次出现位置是-1(即未出现),则整个搜索词后移6 - (-1) = 7
位。
依然从尾部开始比较,”E”与”E”匹配。
比较前面一位,”LE”与”LE”匹配。
比较前面一位,”PLE”与”PLE”匹配。
比较前面一位,”MPLE”与”MPLE”匹配。我们把这种情况称为”好后缀”(good suffix),即所有尾部匹配的字符串。注意,”MPLE”、”PLE”、”LE”、”E”都是好后缀。
比较前一位,发现”I”与”A”不匹配。所以,”I”是”坏字符”。
根据”坏字符规则”,此时搜索词应该后移2 - (-1)= 3
位。问题是,此时有没有更好的移法?
我们知道,此时存在”好后缀”。所以,可以采用”好后缀规则”:1
后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置
计算时,位置的取值以”好后缀”的最后一个字符为准。如果”好后缀”在搜索词中没有重复出现,则它的上一次出现位置为-1。
所有的”好后缀”(MPLE、PLE、LE、E)之中,只有”E”在”EXAMPLE”之中出现两次,所以后移6 - 0 = 6
位。
可以看到,”坏字符规则”只能移3位,”好后缀规则”可以移6位。所以,Boyer-Moore
算法的基本思想是,每次后移这两个规则之中的较大值。
更巧妙的是,这两个规则的移动位数,只与搜索词有关,与原字符串无关。因此,可以预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下就可以了。
继续从尾部开始比较,”P”与”E”不匹配,因此”P”是”坏字符”。根据”坏字符规则”,后移6 - 4 = 2
位。
从尾部开始逐位比较,发现全部匹配,于是搜索结束。如果还要继续查找(即找出全部匹配),则根据”好后缀规则”,后移6 - 0 = 6
位,即头部的”E”移到尾部的”E”的位置。
特点
Boyer-Moore 算法的主要特点有:
- 对模式字符的比较顺序时从右向左。
- 预处理需要
O(m + σ)
的时间和空间复杂度。 - 匹配阶段需要
O(m × n)
的时间复杂度。 - 匹配阶段在最坏情况下需要
3n
次字符比较。 - 最优复杂度
O(n/m)
。概念
Boyer–Moore
算法在对模式P字符串进行预处理时,将采用两种不同的启发式方法。这两种启发式的预处理方法称为:
- 坏字符(Bad Character Heuristic):当文本 T 中的某个字符跟模式P的某个字符不匹配时,我们称文本T中的这个失配字符为坏字符。
- 好后缀(Good Suffix Heuristic):当文本 T 中的某个字符跟模式P的某个字符不匹配时,我们称文本T中的已经匹配的字符串为好后缀。
Boyer–Moore算法在预处理时,将为两种不同的启发法结果创建不同的数组,分别称为 Bad-Character-Shift(or The Occurrence Shift)和Good-Suffix-Shift(or Matching Shift)。当进行字符匹配时,如果发现模式P中的字符与文本T中的字符不匹配时,将比较两种不同预处理方法,选择最大的一个值来对模式串(搜索词)P的位置进行滑动。
坏字符
当出现一个坏字符时, BM算法向右移动模式串, 让模式串中最靠右的对应字符(对应的坏字符串)与坏字符相对,然后继续匹配。坏字符算法有两种情况。
- 模式串中有对应的坏字符时,让模式串中最靠右的对应字符(对应的坏字符串)与坏字符相对(PS:BM不可能走回头路,因为若是回头路,则移动距离就是负数了,肯定不是最大移动步数了),如下图。
- 模式串中不存在文本串的坏字符,直接右移模式串(未匹配的,示例:aba移动到c位置后)长度。
为了用代码来描述上述的两种情况,设计一个数组bmBc['k']
,表示坏字符‘k’在模式串中出现的位置距离模式串末尾的最大长度,那么当遇到坏字符的时候,模式串可以移动距离为:shift(坏字符) = bmBc[T[i]]-(m-1-i)
。好后缀
有三种情况: - 模式串中有子串匹配上好后缀,此时移动模式串,让该文本串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠左边的子串对齐。
- 模式串中没有文本串匹配上好后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可。
- 模式串中没有文本串匹配上好后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。此时,直接移动模式串到好后缀(位置的取值以”好后缀”的最后一个字符为准)的下一个字符。
代码
C++代码
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234#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 pattern[256] ={ 'b','a','b','c','d','e'};
//char text[256]={'a','b','c','d','e','f','g','b','c','d','e'};
char text[256] ={ 'I','a','m','h','a','p','p','y'},pattern[256]={'a','p',};
BoyerMoore(pattern, strlen(pattern), text, strlen(text));
/*char pattern[256]={'e','x','a','m','p','l','e',};
int suff[SIZE];
int i, j,m=strlen(pattern);*/
return 0;
}
//@pattern:搜索串
//@m:搜索串长度
//计算坏字符数组
//一个数组bmBc['k'],表示坏字符‘k’在模式串中出现的位置距离模式串末尾的最大长度,那么当遇到坏字符的时候,模式串可以移动距离为: shift(坏字符) = bmBc[T[i]]-(m-1-i)
void PreBmBc(char *pattern, int m, int bmBc[])
{
int i;
//条件2:坏字符没有出现在搜索串中,移动整个搜索串(初始化的时候默认移动)
//初始化bmBc数据
for (i = 0; i < MAX_CHAR; i++){
bmBc[i] = m;
}
//条件1
//bmBc存字符:似乎只需要bmBc[i] = m - 1 - i就行了,但这样是不对的,因为i位置处的字符可能在pattern中多处出现,而我们需要的是最右边的位置,这样就需要每次循环判断了,
//非常麻烦,性能差。这里有个小技巧,就是使用字符作为下标而不是位置数字作为下标。这样只需要遍历一遍即可,这是空间换时间的做法.
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");
}
//打印方法
//array:字符数组指针
//n长度
//字符数组名字
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");
}
//计算好后缀数组
//@pattern:模式串
//@m:模式串长度
//@suff:suffix[i] = s 表示以i为边界的字符后缀,与模式串后缀匹配的最大长度。(pattern中以i位置字符为后缀和以最后一个字符为后缀的公共后缀串的长度)
void suffix_old(char *pattern, int m, int suff[])
{
//i:从后往前比较的标记位,好后缀最后1个字符的位置
//j: 在搜索串中下标位置比较,表示好后缀在搜索词中上1次出现的位置
int i, j;
//定义最后一个字符需要移动的距离:整个搜索串。例子:最后1个字符是好后缀,且没有在搜索串中出现,最后一个字符移动距离=(m-1)-(-1)=m
//suffix[i] = s 表示以i为边界的字符后缀,与模式串后缀匹配的最大长度。
suff[m - 1] = m;
//从最后一个串开始比较
for (i = m - 2; i >= 0; i--)
{
j = i;
//2个字符匹配,继续往前找字符
//j<(m-1-i+j):相差1位
//如果j与j+1,不相同j=i=i--,m-1-i+j也就是最后1个字符,如果最后1个字符找相同的字符
while (j >= 0 && pattern[j] == pattern[m -1 - i + j]){
j--;
}
//获取匹配的最大长度
//i:好后缀最后1个字符的位置
//suff[i]:i位置需要移动的距离
//j:i位置的字符出现在搜索串中上1个的位置
//非好后缀,suff的值是0,不做移动
suff[i] = i - j;
}
}
//改进计算suffix方法
void suffix(char *pattern, int m, int suff[])
{
int f, g, i;
suff[m - 1] = m;
//上一次进行成功匹配的失配位置
g = m - 1;
//i开始匹配的位置
for (i = m - 2; i >= 0; --i)
{
//f:上一个成功进行匹配的起始位置
if (i > g && suff[i + m - 1 - f] < i - g){
//获取匹配的最大长度
suff[i] = suff[i + m - 1 - f];
}
else
{
//g的最大值就是i
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[]");
}
//好后缀规则
//@pattern:搜索串
//@m:搜索串长度
//@bmGs: bmGs[i] 表示遇到好后缀时,搜索串应该移动的距离,其中i表示好后缀前面一个字符的位置
void PreBmGs(char *pattern, int m, int bmGs[])
{
int i, j;
int suff[SIZE];
// 计算后缀数组
suffix(pattern, m, suff);
//suffix_old(pattern, m, suff);
//条件3:模式串中没有子串匹配上好后缀
//以好后缀最后1个字符位置为标准,移动距离:(m-1)-(-1)
for (i = 0; i < m; i++){
bmGs[i] = m;
}
//条件2:模式串有前缀字符串匹配好后缀的后缀
//范围内的任意位置:模式串前缀匹配好后缀后1位置开始-好后缀的前一个字符串标记位
j = 0;
//i:从尾部往前比较
//i:好后缀的前1个位置
for (i = m - 1; i >= 0; i--)
{
//i等于其最大匹配长度,说明i位置的字符没有在模式串中重复出现
if (i + 1 == suff[i])
{
//j从前往后比较
//模式串的前缀的位置开始->模式串后缀前1个位置
for (;j < m - 1 - i; j++)
{
//如果j的最大移动位置不是m,说明j的位置字符串在模式串中重复存在,j的位置就是在好后缀范围内。根据公式:后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置
//如果j的最大移动位置是m,说明j不在好后缀的范围内
if (m == bmGs[j]){
//j位置移动的最大距离.好后缀在模式串匹配的前缀+1位置开始移动
bmGs[j] = m - 1 - i;
}
}
}
}
//条件1
for (i = 0; i <= m - 2; i++){
int k=m - 1 - suff[i];
int p=m - 1 - i;
//suff[i]:i位置的字符后缀与搜索串最后1个字符的后缀相同的最大长度,好后缀的最大长度
//在位置:m - 1 - suff[i],需要移动的距离:m-1-i
bmGs[k] = p;
}
print(bmGs, m , "bmGs[]");
}
//@pattern:搜索词字符串
//@m:搜索词字符串长度
//@text:文本字符串
//@m:文本字符串长度
void BoyerMoore(char *pattern, int m, char *text, int n)
{
int i, j, bmBc[MAX_CHAR], bmGs[SIZE];
//获取坏字符
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--){
printf("\n");
};
//找到第一个不匹配的位置,然后说明是最好后缀模式,输出模式串的移动距离
//i=0;i在搜索串第1个字符上,i--:在搜索字符串外的第1个位置
if (i < 0)
{
printf("Find it, the position is %d\n", j);
//在搜索字符串0这个位置需要移动的距离
j += bmGs[0];
return ;
}
else{
//坏字符规则:后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置
//选择最大的一个值来对模式串(搜索词) 的位置进行滑动.
//坏字符串移动的距离:bmBc[text[i + j]] - (m - 1 - i),bmBc[text[i + j]]:坏字符在搜索字符串中出现的位置到搜索字符串末端的最大距离
//好后缀移动的距离:bmGs[i]
//i在搜索串这个位置,搜索串需要移动的距离
j += MAX(bmBc[text[i + j]] - m + 1 + i, bmGs[i]);
}
}
printf("No find.\n");
}
Java代码1
1 | package demo1; |
Java代码2
1 | package demo1; |
Java代码3
1 | package demo1; |
代码解释
坏字符
定义一个bmBc
数组:一个数组bmBc['k']
,表示坏字符’k’在模式串中出现的位置距离模式串末尾的最大长度。
这个计算应该很容易,似乎只需要bmBc[i] = m - 1 - i
就行了,但这样是不对的,因为i位置处的字符可能在模式串中多处出现(如下图所示),而我们需要的是最右边的位置,这样就需要每次循环判断了,非常麻烦,性能差。这里有个小技巧,就是使用字符作为下标而不是位置数字作为下标。这样只需要遍历一遍即可,这是空间换时间的做法。
条件1:字符在模式串中有出现,bmBc['v']
表示字符v在模式串中最后一次出现的位置(期间有个循环),距离模式串串尾的长度,如上图所示。1
2
3
4for (i = 0; i < m - 1; i++){
bmBc[pattern[i]] = m - 1 - i; //bmBc['k'],表示坏字符‘k’在模式串中出现的位置距离模式串末尾的最大长度;pattern[i]:模式串单个字符;
//表示:第i个字符,是坏字符,距离模式串末尾长度
}
条件2:坏字符在模式串中没有出现,如模式串中没有字符v,则BmBc['v'] = strlen(pattern)
。1
2for (i = 0; i < MAX_CHAR; i++)
bmBc[i] = m; //没有一个匹配,移动整个模式串
模式串实际要右移的距离就是:bmBc['v'] - m + 1 + i
。
好后缀
suffix
的方法:定义一个suffix
数组,该数组的意思是suffix[i] = s
表示以i为边界的字符后缀,与模式串后缀匹配的最大长度。(pattern
中以i位置字符为后缀和以最后一个字符为后缀的公共后缀串的长度)
定义bmGs[]数组:bmGs[i]
表示遇到好后缀时,模式串应该移动的距离,其中i表示好后缀前面一个字符的位置。
条件1:模式串中有子串匹配上好后缀
或者1
2
3
4
5//条件1
for (i = 0; i <= m - 2; i++){
//在位置:m - 1 - suff[i],需要移动的距离:m-1-i
bmGs[m - 1 - suff[i]] = m - 1 - i; //从前往后搜索匹配的模式串,i为好后缀边界的字符
}
条件2:模式串中没有子串匹配上好后缀,但找到一个最大前缀
或者1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 //条件2:模式串有前缀字符串匹配
//好后缀的前一个字符串标记位
j = 0;
//i:从尾部往前比较
for (i = m - 1; i >= 0; i--)
{
//i等于其最大匹配长度,说明:i是在模式串的匹配位置上
//
if (i + 1 == suff[i])
{
//j从前往后比较,从空白部分(如图)开始++
for (;j < m - 1 - i; j++)
{
//
if (m == bmGs[j]){
bmGs[j] = m - 1 - i;//假设j是在de后面的位置,也就是位置2,如果在位置2,de需要往后移动18,才能和de相匹配。所以则由:m-1-i
}
}
}
}
条件3:模式串中没有子串匹配上好后缀
或者1
2
3
4//条件3,每一个字符都是移动m的距离
for (i = 0; i < m; i++){
bmGs[i] = m; //每个字符的最好后缀都是移动整个模式串
}
改进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
了吗。
PS:这里有些人可能觉得应该是suff[m-1-f+i] <= i – g
,因为若suff[m-1-f+i] = i – g
,还是没超过suff[f]
的范围,依然可以利用前面的suff[]
,但这是错误的,比如一个极端的例子。1
2i :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可能会在这次得到匹配。suff[]
与模式串后缀匹配的最大长度。g<i<f,P[i]=P[m-1-f+i]
,这是相等的。比如:以最后一个字符开始,f=m-1
。如果是以前半段那个字符开始。m-1-f
截取中间一半,然后+i,则到下半部分,下半部分的i和上半部分的i是同一个字符。suff[m-1-f+i] < i-g
,相当于i在偏f方向。
suff数组
在计算bmGc
数组时,为提高效率,先计算辅助数组suff[]
表示好后缀的长度。
suff数组的定义:m是pattern
的长度。1
2
3a. 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
。