BM算法详解及Java实现.docx
《BM算法详解及Java实现.docx》由会员分享,可在线阅读,更多相关《BM算法详解及Java实现.docx(16页珍藏版)》请在冰豆网上搜索。
BM算法详解及Java实现
BM算法详解及Java实现
1977年,德克萨斯大学的RobertS.Boyer教授和JStrotherMoore教授发明了一种新的字符串匹配算法:
Boyer-Moore算法,简称BM算法。
该算法从模式串的尾部开始匹配,且拥有在最坏情况下O(N)的时间复杂度。
在实践中,比KMP算法的实际效能高。
一、后缀暴力匹配算法
后缀匹配,是指模式串的比较从右到左,模式串的移动也是从左到右的匹配过程,经典的BM算法其实是对后缀暴力匹配算法的改进。
所以还是先从最简单的后缀暴力匹配算法开始。
下面直接给出伪代码,注意这一行代码:
j++;BM算法所做的唯一的事情就是改进了这行代码,即模式串不是每次移动一步,而是根据已经匹配的后缀信息,从而移动更多的距离。
/**
*后缀暴力匹配
*
*@paramT正文字符数组
*@paramP模式字符数组
*@return匹配位置
*/
publicstaticintbf(char[]T,char[]P){
for(intj=0;j<=T.length-P.length;j++){
inti=P.length-1;
for(;i>=0&&P[i]==T[i+j];--i){
}
if(i<0){
returnj;
}
}
return-1;
}
二、BM算法介绍
为了实现更快移动模式串,BM算法定义了两个规则,好后缀规则和坏字符规则,如下图可以清晰的看出他们的含义。
利用好后缀和坏字符可以大大加快模式串的移动距离,不是简单的++j,而是j+=max(shift(好后缀),shift(坏字符))。
下面举例说明BM算法。
例如,给定文本串“HEREISASIMPLEEXAMPLE”,和模式串“EXAMPLE”,现要查找模式串是否在文本串中,如果存在,返回模式串在文本串中的位置。
1.首先,"文本串"与"模式串"头部对齐,从尾部开始比较。
"S"与"E"不匹配。
这时,"S"就被称为"坏字符"(badcharacter),即不匹配的字符,它对应着模式串的第6位。
且"S"不包含在模式串"EXAMPLE"之中(相当于最右出现位置是-1),这意味着可以把模式串后移6-(-1)=7位,从而直接移到"S"的后一位。
2.依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。
但是,"P"包含在模式串"EXAMPLE"之中。
因为“P”这个“坏字符”对应着模式串的第6位(从0开始编号),且在模式串中的最右出现位置为4,所以,将模式串后移6-4=2位,两个"P"对齐。
3. 依次比较,得到“MPLE”匹配,称为"好后缀"(goodsuffix),即所有尾部匹配的字符串。
注意,"MPLE"、"PLE"、"LE"、"E"都是好后缀。
4.发现“I”与“A”不匹配:
“I”是坏字符。
如果是根据坏字符规则,此时模式串应该后移2-(-1)=3位。
5.继续从尾部开始比较,“X”与“E”不匹配,因此“X”是“好字符”,根据“好字符规则”,后移6-1=5位。
这是,“X”与“X”对齐。
6.依次从尾部开始比较,所有字段都能匹配,查找成功。
由上可知,BM算法不仅效率高,而且构思巧妙,容易理解。
三、坏字符算法
先来看如何根据坏字符来移动模式串,shift(坏字符)分为两种情况:
(1)坏字符没出现在模式串中,这时可以把模式串移动到坏字符的下一个字符,继续比较,如下图:
(2)坏字符出现在模式串中,这时可以把模式串第一个出现的坏字符和母串的坏字符对齐,当然,这样可能造成模式串倒退移动,如下图:
此处配的图是不准确的,因为显然加粗的那个b并不是”最靠右的”b。
而且也与下面给出的代码冲突!
我看了论文,论文的意思是最右边的。
当然了,尽管一时大意图配错了,论述还是没有问题的,我们可以把图改正一下,把圈圈中的b改为字母f就好了。
接下来的图就不再更改了,大家心里有数就好。
为了用代码来描述上述的两种情况,设计一个数组bmBc['k'],表示坏字符‘k’在模式串中出现的位置距离模式串末尾的最大长度,那么当遇到坏字符的时候,模式串可以移动距离为:
shift(坏字符)=bmBc[T[i]]-(m-1-i)。
如下图:
数组bmBc的创建非常简单,直接贴出代码如下:
/**
*坏字符忽略映射
*
*@paramT模式字符数组
*@return坏字符忽略映射
*/
publicstaticint[]bmBc(char[]T){
//初始忽略数组
int[]bmBc=newint[65536];
//填充坏字符序号
Arrays.fill(bmBc,-1);
//填充好字符序号(以最后位置为准)
intm=T.length;
for(inti=0;ibmBc[T[i]]=m-1-i;
}
//返回忽略数组
returnbmBc;
}
代码说明:
为了方便起见,就直接把UTF8编码用65536个字符全表示了,这样就不会漏掉哪个字符了。
for循环,bmBc[x[i]]中x[i]表示模式串中的第i个字符。
bmBc[x[i]]=m-i-1;也就是计算x[i]这个字符到串尾部的距离。
为什么循环中,i从小到大的顺序计算呢?
技巧就在这儿了,原因在于就可以在同一字符多次出现的时候以最靠右的那个字符到尾部距离为最终的距离。
当然了,如果没在模式串中出现的字符,其距离就是m了。
四、好后缀算法
再来看如何根据好后缀规则移动模式串,shift(好后缀)分为三种情况:
(1)模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠左边的子串对齐。
(2)模式串中没有子串匹配上后后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可。
(3)模式串中没有子串匹配上后后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。
此时,直接移动模式到好后缀的下一个字符。
为了实现好后缀规则,需要定义一个数组suffix[],其中suffix[i]=s表示以i为边界,与模式串后缀匹配的最大长度,如下图所示,用公式可以描述:
满足P[i-s,i]==P[m-s,m]的最大长度s。
构建suffix数组的代码如下:
/**
*计算后缀长度数组
*
*@paramP模式字符数组
*@return后缀长度数组
*/
publicstaticint[]suffix(char[]P){
//初始后缀数组
intm=P.length;
int[]suffix=newint[m];
//计算后缀数组
suffix[m-1]=m;
for(inti=m-2;i>=0;--i){
intq=i;
while(q>=0&&P[q]==P[m-1-i+q]){
q--;
}
suffix[i]=i-q;
}
//返回后缀数组
returnsuffix;
}
有了suffix数组,就可以定义bmGs[]数组,bmGs[i]表示遇到好后缀时,模式串应该移动的距离,其中i表示好后缀前面一个字符的位置(也就是坏字符的位置),构建bmGs数组分为三种情况,分别对应上述的移动模式串的三种情况:
(1)模式串中有子串匹配上好后缀
(2)模式串中没有子串匹配上好后缀,但找到一个最大前缀:
(3)模式串中没有子串匹配上好后缀,但找不到一个最大前缀。
构建bmGs数组的代码如下:
/**
*好后缀忽略映射
*
*@paramT模式字符映射
*@return后缀数组(字符对应模式字符数组中的最后位置)
*/
publicstaticint[]bmgs(char[]P){
//初始忽略数组
intm=P.length;
int[]bmgs=newint[m];
//获取后缀数组
int[]suffix=suffix(P);
//赋值默认取值
Arrays.fill(bmgs,m);
//模式串中没有子串匹配上好后缀,但找到一个最大前缀
for(inti=m-1,j=0;i>=0;--i){
if(suffix[i]==i+1){
for(;jif(bmgs[j]==m){
bmgs[j]=m-1-i;
}
}
}
}
//模式串中有子串匹配上好后缀
for(inti=0;i<=m-2;++i){
bmgs[m-1-suffix[i]]=m-1-i;
}
//返回忽略数组
returnbmgs;
}
代码说明:
这一部分代码挺有讲究,写的很巧妙,这里谈谈我的理解。
讲解代码时候是分为三种情况来说明的,其实第二种和第三种可以合并,因为第三种情况相当于与好后缀匹配的最长前缀长度为0。
由于我们的目的是获得精确的bmGs[i],故而若一个字符同时符合上述三种情况中的几种,那么我们选取最小的bmGs[i]。
比如当模式传中既有子串可以匹配上好后串,又有前缀可以匹配好后串的后串,那么此时我们应该按照前者来移动模式串,也就是bmGs[i]较小的那种情况。
故而每次修改bmGs[i]都应该使其变小,记住这一点,很重要!
而在这三种情况中第三种情况获得的bmGs[i]值大于第二种大于第一种。
故而写代码的时候我们先计算第三种情况,再计算第二种情况,再计算第一种情况。
为什么呢,因为对于同一个位置的多次修改只会使得bmGs[i]越来越小。
代码4-5行对应了第三种情况,7-11行对于第二种情况,12-13对应第一种情况。
第三种情况比较简单直接赋值m,这里就不多提了。
第二种情况有点意思,咱们细细的来品味一下。
1. 为什么从后往前,也就是i从大到小?
原因在于如果i,j(i>j)位置同时满足第二种情况,那么m-1-i2.第8行代码的意思是找到了合适的位置,为什么这么说呢?
因为根据suff的定义,我们知道
x[i+1-suff[i]…i]==x[m-1-siff[i]…m-1],而suff[i]==i+1,我们知道x[i+1-suff[i]…i]=x[0,i],也就是前缀,满足第二种情况。
3.第9-11行就是在对满足第二种情况下的赋值了。
第十行确保了每个位置最多只能被修改一次。
第12-13行就是处理第一种情况了。
为什么顺序从前到后呢,也就是i从小到大?
原因在于如果suff[i]==suff[j],im-1-j,我们应该取后者作为bmGs[m-1-suff[i]]的值。
举例说明:
五、BM核心算法
BM算法代码如下:
/**
*BM算法匹配
*
*@paramT正文字符数组
*@paramP模式字符数组
*@return匹配位置
*/
publicstaticintbm(char[]T,char[]P){
//获取忽略映射
int[]bmBc=bmBc(P);
int[]bmGs=bmGs(P);
//匹配字符串
intj=0;
intn=T.length;
intm=P.length;
while(j<=n-m){
//倒序匹配字符
inti=m-1;
while(i>=0&&P[i]==T[i+j]){
i--;
}
//成功匹配字符
if(i<0){
returnj;
}
//往后移动位置
j+=Math.max(bmGs[i],bmBc[T[i+j]]-m+1+i);
}
//返回查找失败
return-1;
}
六、BM算法测试
主函数:
/**
*主函数
*
*@paramargs参数数组
*/
publicstaticvoidmain(String[]args){
//定义原始数据
Stringtext="HEREISASIMPLEEXAMPLE";
Stringpattern="EXAMPLE";
System.out.println("T:
\t"+text);
System.out.println("P:
\t"+pattern);
//测试坏字符忽略映射
int[]bmBc=bmBc(pattern.toCharArray());
System.out.println("坏字符忽略映射:
");
for(inti=0;iif(bmBc[i]!
=-1){
System.out.println("bmBc['"+(char)i+"']:
"+bmBc[i]);
}
}
//测试好后缀数组映射
int[]suffix=suffix(pattern.toCharArray());
System.out.println("好后缀数组映射:
");
for(inti=0;iSystem.out.println("suffix["+i+"]:
"+suffix[i]);
}
//测试好后缀忽略映射
int[]bmGs=bmGs(pattern.toCharArray());
System.out.println("好后缀忽略映射:
");
for(inti=0;iSystem.out.println("bmGs["+i+"]:
"+bmGs[i]);
}
//测试BM匹配算法结果
System.out.println("BM:
\t"+bm(text.toCharArray(),pattern.toCharArray()));
System.out.println("BF:
\t"+bf(text.toCharArray(),pattern.toCharArray()));
System.out.println("String:
\t"+text.indexOf(pattern));
}
运行结果:
T:
HEREISASIMPLEEXAMPLE
P:
EXAMPLE
坏字符忽略映射:
bmBc['A']:
4
bmBc['E']:
0
bmBc['L']:
1
bmBc['M']:
3
bmBc['P']:
2
bmBc['X']:
5
好后缀数组映射:
suffix[0]:
1
suffix[1]:
0
suffix[2]:
0
suffix[3]:
0
suffix[4]:
0
suffix[5]:
0
suffix[6]:
7
好后缀忽略映射:
bmGs[0]:
6
bmGs[1]:
6
bmGs[2]:
6
bmGs[3]:
6
bmGs[4]:
6
bmGs[5]:
6
bmGs[6]:
1
BM:
17
BF:
17
String:
17