字符串匹配算法很多,Boyer-Moore算法也不算是效率最高的算法,它常用于各种文本编辑器的”查找”功能(Ctrl+F)。
比较经典的字符串模式匹配算法还有:Horspool算法、Sunday算法、KR算法、AC自动机等。不多说,进入主题。
Boyer-Moore算法概率
假定字符串为”HERE IS A SIMPLE EXAMPLE”,搜索词为”EXAMPLE”。
首先,”字符串”与”搜索词”头部对齐,从尾部开始比较。
这是一个很聪明的想法,因为如果尾部字符不匹配,那么只要一次比较,就可以知道前7个字符肯定不是要找的结果。
我们看到,”S”与”E”不匹配。这时,”S”就被称为”坏字符”(bad character),即不匹配的字符。我们还发现,”S”不包含在搜索词”EXAMPLE”之中,这意味着可以把搜索词直接移到”S”的后一位。依然从尾部开始比较,发现”P”与”E”不匹配,所以”P”是”坏字符”。但是,”P”包含在搜索词”EXAMPLE”之中。所以,将搜索词后移两位,两个”P”对齐。
我们由此总结出”坏字符规则”:后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置
如果”坏字符”不包含在搜索词之中,则上一次出现位置为 -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。
所有的”好后缀”(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算法实现
/*
函数:int* MakeBadCharTable(char *, int)
目的:根据坏字符规则做预处理,建立一张坏字符表
参数:
p => 模式串P
PLen => 模式串P长度
返回:
int* - 坏字符表
*/
int* MakeBadCharTable(char *p, int pLen)
{
int i;
//为建立坏字符表,申请256个int的空间
/*之所以要申请256个,是因为一个字符是8位,所以字符可能有2的8次方即256种不同情况*/
int *badchar = (int*)malloc(256*sizeof(int));
if(badchar == NULL){
printf("malloc failed!");
return 0;
}
//初始化坏字符表,256个单元全部初始化为pLen,没有在模式串出现的字符距离为pLen。
for(i = 0; i < 256; i++){
*(badchar+i) = -1;
}
//给表中需要赋值的单元赋值,不在模式串中出现的字符就不用再赋值了
//以数组小标为字符键,以值为字符坏字符位置
while(pLen != 0){
*(badchar+(unsigned char)*p++) = pLen--;
}
return badchar;
}
/*
函数:int* MakeGoodShiftTable(char *, int)
目的:根据好后缀规则做预处理,建立一张好后缀表
参数:
p => 模式串P
PLen => 模式串P长度
返回:
int* - 好后缀表
*/
int* MakeGoodShiftTable(char* p,int pLen){
//为好后缀表申请pLen个int的空间
int *shift = (int*)malloc(pLen*sizeof(int));
int *sptr = shift + pLen - 1;//方便给好后缀表进行赋值的指标
char *pptr = p + pLen - 1;//记录好后缀表边界位置的指标
char c;
if(shift == NULL){
fprintf(stderr,"malloc failed!");
return 0;
}
c = *(p + pLen - 1);//保存模式串中最后一个字符,因为要反复用到它
*sptr = 1;//以最后一个字符为边界时,确定移动1的距离
pptr--;//边界移动到倒数第二个字符(这句是我自己加上去的,因为我总觉得不加上去会有BUG,大家试试“abcdd”的情况,即末尾两位重复的情况)
while(sptr-- != shift){//该最外层循环完成给好后缀表中每一个单元进行赋值的工作
char *p1 = p + pLen - 2, *p2,*p3;
//该do...while循环完成以当前pptr所指的字符为边界时,要移动的距离
do{
while(p1 >= p && *p1-- != c);//该空循环,寻找与最后一个字符c匹配的字符所指向的位置
p2 = p + pLen - 2;
p3 = p1;
while(p3 >= p && *p3-- == *p2-- && p2 >= pptr);//该空循环,判断在边界内字符匹配到了什么位置
}while(p3 >= p && p2 >= pptr);
*sptr = shift + pLen - sptr + p2 - p3;//保存好后缀表中,以pptr所在字符为边界时,要移动的位置
/*
PS:在这里我要声明一句,*sptr = (shift + pLen - sptr) + p2 - p3;
大家看被我用括号括起来的部分,如果只需要计算字符串移动的距离,那么括号中的那部分是不需要的。
因为在字符串自左向右做匹配的时候,指标是一直向左移的,这里*sptr保存的内容,实际是指标要移动
距离,而不是字符串移动的距离。我想SNORT是出于性能上的考虑,才这么做的。
*/
pptr--;//边界继续向前移动
}
return shift;
}
/*
函数:int* BMSearch(char *, int , char *, int, int *, int *)
目的:判断文本串T中是否包含模式串P
参数:
buf => 文本串T
blen => 文本串T长度
ptrn => 模式串P
PLen => 模式串P长度
skip => 坏字符表
shift => 好后缀表
返回:
int - 1表示成功(文本串包含模式串),0表示失败(文本串不包含模式串)。
*/
int BMSearch(char *buf, int blen, char *ptrn, int plen, int *badchar, int *goodshift){
int b_idx = plen;
if (plen == 0)
return 1;
while (b_idx <= blen){//计算字符串是否匹配到了尽头
int p_idx = plen, skip_stride, shift_stride;
while (buf[--b_idx] == ptrn[--p_idx]){//开始匹配
if (b_idx < 0)
return 0;
if (p_idx == 0){
return 1;
}
}
skip_stride = badchar[(unsigned char)buf[b_idx]];//根据坏字符规则计算跳跃的距离
shift_stride = goodshift[p_idx];//根据好后缀规则计算跳跃的距离
b_idx += (skip_stride > shift_stride) ? skip_stride : shift_stride;//取大者
}
return 0;
}
这个算法理解起来,比KMP容易多了,但是整体还是很复杂。效率看似不比KMP高多少,但是在实际应用的数据中,效率却高很多。
最后,概念和代码分别来自互联网,这里我只是做了一个比较全面的总结。我想重点在于理解算法的思路吧。
http://kb.cnblogs.com/page/176945/