BM(Boyer-Moore)算法:是一种高效的字符串匹配算法,性能是相当之高,是KMP的几倍之多。
术语
在123456abc789中找abc
主串:123456abc789为主串
模式串:abc为字串
BM算法思想
通过增加匹配失败后一次移动的字符数,减少无效的匹配次数,从而增加匹配效率。
如何在增加移动字符数
BM算法充分利用了模式串的不变特性,和在发生不匹配时,已经匹配了的字符串及已知的不匹配字符串
a | b | c | a | c | a | b | c | b | c |
a | b | c | b | c |
图1
这里引入两个概念
1、坏字符:图1红色字体a就是不匹配的坏字符
2、好后缀:图1的绿色字体bc就是好后缀
BM算法原理
坏字符规则
根据坏字符和模式串,BM算法是如何一次移动多位的,如何增加效率的
我们试着逐步移动,将模式串向右移动一位
a | b | c | a | c | a | b | c | b | c |
a | b | c | b | c |
图2
其他的我们先不考虑,我们分析我们已知的坏字符,与移位后的对应模式串上的b是不匹配的,我们继续右移一位
a | b | c | a | c | a | b | c | b | c |
a | b | c | b | c |
图3
移位后主串上坏字符,与移位后的模式串对应位置的字符是匹配的。
从上面分析我们可知,在匹配出现坏字符时,我们需要右移,而模式串中与当前坏字符位置(即a)可匹配的位置,可以自模式串右边向左边逐一比较获取,这样我们就一次移动到模式串与坏字符匹配的位置了。
而逐一向前对比结果是增加了模式串向前移动的字符数,但是还是需要对比O(m)次,性能并没有提高,而模式串内容是固定的,我们可以把模式串中每个字符在模式串中的位置通过hash表缓存起来,
hash算法:
哈希函数:我们可以直接取字符的ascii码值
所以将字符的ascii码值作为数组下标,对应的在模式串中的位置作为值,ascii是一个字符大小,所以申请255大小的数组bc,数组的初始化代码如下:
private void generateBC() {
//所有bc初始化为-1
for (int i = 0; i < bc.length; i++) {
bc[i] = -1;
}
for (int i = 0; i < b.length; i++) {
// 将字串i处内容的ascii码值作为缓存下标,其下标作为缓存的值
int ascii = b[i];
bc[ascii] = i;
}
}
如此上面就不要逐一比较了,直接使用bc[a]=0就能找到最近的匹配位置。
图4
根据上面的图坏字符=a,s=2,模式串中对应匹配x=bc[a]=0,
向右移动字符数=S-X=2-0=2,
坏字符缺点:
图5
如果匹配串如上图,坏字符在模式串中的匹配位置在右边即X=bc[a]=3, 移动字符数=S-X=2-3=-1,
所以要配合好后缀一起使用
好后缀规则:
好后缀的规则比较复杂一些,但基本上和坏字符原理类似。
a | b | c | a | c | b | b | c | b | c |
a | b | c | d | b | c |
图6
匹配好的(绿色字体)bc是好后缀,记作{u},拿bc到模式串中查找,如果找到另一个跟{u}匹配的字串{u*},我们就可以将模式串移动到{u*}的起始位置。
图7
如图7:移动字符数=S-GS+1
我们再来看看图6应该向右移动多少:3-1+1=3,移动后结果为
a | b | c | a | c | b | b | c | b | c | |
a | b | c | d | b | c |
图8
另一种情形,如果好的后缀在模式串中没有找到另一个匹配字符串,是否就可以直接将模式串滑动到{u}后面,我们来看看下面这种情形
a | b | c | a | c | b | c | d | b | c |
c | d | b | c |
图9
上图9,如果在模式串中没有找到对应的,直接将模式串启动到bc之后,将会错过匹配串。
a | b | c | a | c | b | c | d | b | c | ||
c | d | b | c | ||||||||
c | d | b | c | 移动到{u}之后 | |||||||
c | d | b | c | 错过的匹配处 |
图10
上图就是出现滑动过度情形,所以处理好的后缀的时候,不仅要看完整的后缀,而且要看后缀的子后缀是否存在跟模式串的前缀子串匹配。
后缀子串及后缀字串字串是否存在匹配前缀字串的缓存表达:
好后缀与坏字符类似,也是使用了缓存数组,这里suffix针对不同长度的后缀,在模式串中对应的匹配索引,及是否存在匹配的前缀字串prefix。
c | d | b | c | d | b |
后缀子串 | 长度 | suffix | prefix | ||
b | 1 | 2 | TRUE | ||
db | 2 | 1 | FALSE | ||
cdb | 3 | 0 | TRUE | ||
bcdb | 4 | -1 | FALSE | ||
dbcdb | 5 | -1 | FALSE |
模式串前缀缓存处理
public void generateGs() {
// 所有suffix初始化位-1,所有prefix初始化为prefix
for (int i = 0; i < b.length-1; i++) {
suffix[i]=-1;
prefix[i] = false;
}
int m = b.length;
for (int i = 0; i < m-1; i++) {
int j = i;
int k = 0;
//两端从长度为1对比,每次符合条件刷新suffix,如此,suffix匹配会是最靠近右端
while (j >= 0 && b[m-1-k] == b[j]) {
j--;
k++;
suffix[k] = j+1;
}
if (j == -1) {
prefix[k] = true;
}
}
}
用cbdcbd这个模式串分析;
- i = 0, j=0,k=0
- b[m-1-k]==b[j] => b[5]==b[0] ? => c==d? 不等, j==-1? 不等,结束
- i=1
- j=1,k=0 b[m-k-1]==b[j] => b[5]==b[1] => d==b?不等,j==-1?不等,结束
- i=2
- j=2,k=0 b[m-k-1]==b[j] => b[5]==b[2] => d==d?等,suffix[1]=j=2
- j=1,k=1 b[m-k-1]==b[j] => b[4]==b[1] => b==b?等, suffix[2]=j=1
- j=0,k=2 b[m-k-1]==b[j] => b[3]==b[0] => c==c?等, suffix[3]=j=0
- j=-1,k=3 => prefix[3]=true
- i=3
- j=3,k=0 => b[5]==b[3] => d==c>不等结束
- i=4
- j=4,k=0 => b[5]==b[4] => d==b,不等结束
从上面代码分析可看出,该部分代码是从位置0,逐步与后缀比对,找到最靠右的与不同长度后缀相同的索引,且如果完全匹配,则prefix为true
BM算法代码实现
public int bm(String str) {
char[] a = str.toCharArray();
generateBC();
generateGs();
//print();
int i = 0;
int n = a.length;
int m = b.length;
while (i <= n - m) {
// 计算坏字符
int j;
// 当前对比的是i位开始,与模式串从右自左的对比
for (j = m-1; j>=0; j--) {
if (a[i+j] != b[j]) {
break;
}
}
// 如果全部比完说明完全匹配
if (j < 0) {
return i;
}
int x = j - bc[a[i+j]];
int y = 0;
if (j < m-1) {
y = moveByGS(j, m);
}
i += x > y ? x : y;
}
return -1;
}
/**
* 计算好后缀
* @param j
* @param m
* @return
*/
private int moveByGS(int j, int m) {
// 好后缀长度
int k = m - j - 1;
//模式串存在一个好后缀匹配的子串,返回其索引
if (suffix[k]!=-1) {
return j-suffix[k]+1;
}
//如果没有找到,则看好后缀子后缀在模式串是否有前缀字串匹配
//使用j+2的原因是因为j为坏字符,j+1是好后缀的起始位置,j+2是好后缀的第一个子后缀,子后缀的位置不超过m-1
for (int r = j+2; r <= m-1; r++) {
if (prefix[m-r]) {
return r;
}
}
return 0;
}
使用下面数据模拟运行上面代码
f | b | c | b | b | c | a | c | b | c | b |
c | b | c | b |
n=11,m=4
- i=0
- j=m-1=3,a[i+j]=b[j] => a[3]=b[3] =>b=b?继续
- j=2,a[2]==b[2] => a[2]=b[2]=> c=c?继续
- j=1,b==b继续
- j=0, f!=b
- 坏字符移动距离x=s-bc[f]=0-(-1)=1
- 好后缀moveByGS
- k=m-j-1 => k = 4-0 -1 = 3
- suffix[3] = -1
- 子串中不存在与好后缀完全匹配的子串{u*}
- 继续寻找好后缀的子后缀,模式串中是否存在前缀与子后缀匹配的
- r=2, prefix[4-2]=prefix[2]=true,所以返回2
- i+=Max(坏字符右移,好后缀右移)=> MAX(-1, 2)=2,i=2
-
f b c b b c a c b c b c b c b
- i=2
- j=3,c!=b
- 坏字符移动距离x=j-bc[a[i+j]]=3-bc[a[5]]=3-bc[c]=3-0=3
- j=m-1,不存在好后缀
- i += MAX(坏字符右移,好后缀右移)=MAX(3,0)=3, i=5
-
f b c b b c a c b c b c b c b
- i=5
- j=3, b==b,继续
- j=2, c==c
- j=1, a!=b
- x = j-bc[a[i+j]=>3-bc[a]=>1-(-1)=2
- y=moveByGS,好后缀=cb
- 好后缀长度=m-j-1=4-1-1=2
- suffix[2]=0,移动字符数=j-suffix[2]+1=1-0+1=2
- i+=MAX(2,2)=2=>i=7
-
f b c b b c a c b c b c b c b
- i=7
- 字符串完全匹配
以上是BM算法代码的单步分析,相信如果前面基本原理没有太理解,通过代码的逐步分析也会对BM算法有着更深一层的理解。
BM算法我们分析的差不多了,从BM算法我们是否可以看出一些程序设计的思想
1、 程序中存在大量量不是很大的重复数据查找,我们可是使用hash算法,使O(n)的时间复杂度一下就到了O(1),即热门数据缓存,
2、合理范围内数据可选择好的散列表,减小查询时间复杂度
BM算法就是充分分析出重复的数据操作,与合理利用已处理过的信息