Boyer-Moore算法,老规矩,Wiki下的介绍如下:
“在计算机科学里,Boyer-Moore字符串搜索算法是一种非常高效的字符串搜索算法。它由Bob Boyer和J Strother Moore设计于1977年。此算法仅对搜索目标字符串(关键字)进行预处理,而非被搜索的字符串。虽然Boyer-Moore算法的执行时间同样线性依赖于被搜索字符串的大小,但是通常仅为其它算法的一小部分:它不需要对被搜索的字符串中的字符进行逐一比较,而会跳过其中某些部分。通常搜索关键字越长,算法速度越快。它的效率来自于这样的事实:对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置。”
然后有篇博文(http://blog.csdn.net/sealyao/article/details/4568167)介绍得很不错,可以学习下。
我就针对这篇博文添加下自己的理解注释(用红字下划线表示)。
1、概述
在用于查找子字符串的算法当中,BM(Boyer-Moore)算法是目前相当有效又容易理解的一种,一般情况下,比KMP算法快3-5倍。
BM算法在移动模式串的时候是从左到右,而进行比较的时候是从右到左的。
常规的匹配算法移动模式串的时候是从左到右,而进行比较的时候也是是从左到右的,基本框架是:
j = 0;
while(j <= strlen(主串)- strlen(模式串)){
for (i = 0;i < strlen(模式串) && 模式串[i] == 主串[i + j]; ++i)
;
if (i == strlen(模式串))
Match;
else
++j;
}
而BM算法在移动模式串的时候是从左到右,而进行比较的时候是从右到左的,基本框架是:
j = 0;
while (j <= strlen(主串) - strlen(模式串)) {
for (i = strlen(模式串) - 1; i >= 0 && 模式串[i] ==主串[i + j]; --i)
if (i < 0)
match;
else
++j;
}
显然BM算法并不是上面那个样子,BM算法的精华就在于++j(这个可以先看代码CheckText类的BmCheck函数,会发现++j被取代了,其它流程都一样)
2、BM算法思想
BM算法实际上包含两个并行的算法,坏字符算法和好后缀算法。这两种算法的目的就是让模式串每次向右移动尽可能大的距离(j+=x,x尽可能的大)。(Wiki上也提到了,匹配模式越长,速度就会越快,也就是这个机理)
几个定义:
例主串和模式串如下:
主串 : mahtavaatalomaisema omalomailuun
模式串: maisemaomaloma
好后缀:模式串中的aloma为“好后缀”。
坏字符:主串中的“t”为坏字符。
好后缀算法
如果程序匹配了一个好后缀, 并且在模式中还有另外一个相同的后缀, 那
把下一个后缀移动到当前后缀位置。好后缀算法有两种情况:
Case1:模式串中有子串和好后缀安全匹配,则将最靠右的那个子串移动到好后缀的位置。继续进行匹配。(继续进行匹配的意思是从新从尾开始匹配。可以优化下,既是判断找到的后缀的左边(前面)字符是否等于坏字符,如果是的话,则该后缀不符合要求,继续向前找,这样可以扩大移动距离。不过这样就不能实现匹配模式和被扫描字符串的分离了,因为要考虑被扫描的字符串。)
Case2:如果不存在和好后缀完全匹配的子串,则在好后缀中找到具有如下特征的最长子串,使得P[m-s…m]=P[0…s]。说不清楚的看图。(也可以参考下下面的求BmGs的Case2的那张图。)
坏字符算法
当出现一个坏字符时, BM算法向右移动模式串, 让模式串中最靠右的对应字符与坏字符相对,然后继续匹配。坏字符算法也有两种情况。
Case2:模式串中不存在坏字符。见图。
(到这里该思考下,如果坏字符是pattern的最后一个字符,该怎样移动。)
移动规则
BM算法的移动规则是:
将概述中的++j,换成j+=MAX(shift(好后缀),shift(坏字符)),即
BM算法是每次向右移动模式串的距离是,按照好后缀算法和坏字符算法计算得到的最大值。
shift(好后缀)和shift(坏字符)通过模式串的预处理数组的简单计算得到。好后缀算法的预处理数组是bmGs[],坏字符算法的预处理数组是BmBc[]。
3、代码分析
定义
BM算法子串比较失配时,按坏字符算法计算模式串需要向右移动的距离,要借助BmBc数组。
注意BmBc数组的下标是字符,而不是数字。
BmBc数组的定义,分两种情况。
1、 字符在模式串中有出现。如下图,BmBc[‘k’]表示字符k在模式串中最后一次出现的位置,距离模式串串尾的长度。
2、 字符在模式串中没有出现:,如模式串中没有字符p,则BmBc[‘p’] = strlen(模式串)。
(这里就关联一下上面提到的,k是模式串的最后一个字符的情况下,是不是还是这样处理了呢?需要分两种情况讨论下比较容易理解。
首先,该字符只在模式串中出现一次,那么这个BmBc值就该赋值为模式串的长度,为什么?可以参考下上面提到的坏字符算法的case2,既是虽然遇到了k这个坏字符,但是因为该模式串中不可能再有其它的k字符,所以可以向后移动patternLen个长度。
其次,也是另一种吧,就是在其它位置还重复出现了该字符。那么,就可以按照上面的定义来处理。
结论,在循环的时候,可以把最后一次字符的处理给忽略掉。)
BM算法子串比较失配时,按好后缀算法计算模式串需要向右移动的距离,要借助BmGs数组。
BmGs数组的下标是数字,表示字符在模式串中位置。
BmGs数组的定义,分三种情况。
(注意i不属于好后缀,i对应被扫描的坏字符)
1、 对应好后缀算法case1:如下图:i是好后缀之前的那个位置。
2、 对应好后缀算法case2:如下图所示:
3、 当都不匹配时,BmGs[i] = strlen(模式串)
在计算BmGc数组时,为提高效率,先计算辅助数组Suff。
Suff数组的定义:suff[i] = 以i为边界, 与模式串后缀匹配的最大长度,即P[i-s...i]=P[m-s…m]如下图:
举例如下:
(这里遇到G为2应该就不会再问为什么不是为0了吧)
分析
用Suff[]计算BmGs的方法。
1) BmGs[0…m-1] = m;(第三种情况)
2) 计算第二种情况下的BmGs[]值:
for(i=0;i
if(-1==i || Suff[i] == i+1)
for(;j < m-1-i;++j)
if(suff[j] == m)
BmGs[j] = m-1-i;
3) 计算第三种情况下BmGs[]值,可以覆盖前两种情况下的BmGs[]值:
for(i=0;i
BmGs[m-1-suff[i]] = m-1-i;
如下图所示:
Suff[]数组的计算方法。
常规的方法:如下,很裸很暴力。(看到这里,我发现自己有道德问题……需要反省,为啥就不能想到别的方法。)
Suff[m-1]=m;
for(i=m-2;i>=0;--i){
q=i;
while(q>=0&&P[q]==P[m-1-i+q])
--q;
Suff[i]=i-q;
}
有聪明人想出一种方法,对常规方法进行改进。基本的扫描都是从右向左。改进的地方就是利用了已经计算得到的suff[]值,计算现在正在计算的suff[]值。
(这里不能理解的话,就好好举个例子,慢慢走下了)
如下图所示:
i是当前正准备计算的suff[]值得那个位置。
f是上一个成功进行匹配的起始位置(不是每个位置都能进行成功匹配的, 实际上能够进行成功匹配的位置并不多)。
q是上一次进行成功匹配的失配位置。
如果i在q和f之间,那么一定有P[i]=P[m-1-f+i];并且如果suff[m-1-f+i]=i-q, suff[i]和suff[m-1-f+i]就没有直接关系了。(这里有错误,不是等于,是大于或等于)
自己包装了下,并且填了点注释的代码如下:(重要函数只有四五个……)
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
#define MAX_PATTEN 100 //匹配模式最大长度
#define MAX_TEXT 200 //需要检查的文本最大长度
#define MAX_CHAR 123 //匹配模式中字符ASCII码最大值-1
//匹配模式类
class BmPattern
{
public:
BmPattern(); //默认构造函数
BmPattern(string pattern); //字符串构造函数
string mPattern; //匹配模式字符串
int mMisChar[MAX_CHAR]; //存放坏字符
int mRigString[MAX_PATTEN]; //存放好后缀
int mSuffer[MAX_PATTEN]; //存放临时后缀
private:
void getMisChar(); //计算坏字符
void getSuffer(); //计算临时后缀
void getRigString(); //计算好后缀
void preProcessing(); //预处理函数
};
BmPattern::BmPattern()
{
//To be continue;
}
BmPattern::BmPattern(string pattern)
{
mPattern = pattern;
preProcessing();
}
void BmPattern::preProcessing()
{
getMisChar();
getSuffer();
getRigString();
}
void BmPattern::getMisChar()
{
int i;
int patternLen = mPattern.size();
//默认都设置为不存在,既是坏字符等于匹配模式长度
for (i = 0; i < MAX_CHAR; i++)
{
mMisChar[i] = patternLen;
}
//设置从开始到导数第二个字符的坏字符
patternLen--;
for (i = 0; i < patternLen; i++)
{
mMisChar[mPattern[i]] = patternLen - i;
}
}
/*
mSuffer[i] = k
含义:max{k | [pattern[i - (k - 1) ... pattern[i]] == [pattern[patternlen - 1 - (k - 1) ... pattern[pattern - 1]]}
可以用两个指针扫描,不需要用到frontBegin及frontEnd变量。
但这里使用了另一种等式。
*/
void BmPattern::getSuffer()
{
int i;
int patternlen = mPattern.size();
int frontBegin = 0; //记录上一次匹配成功的起始位置
int frontEnd = patternlen - 1; //记录上一次匹配成功的失配位置
mSuffer[patternlen - 1] = patternlen;
//设置从导数第二个字符到开始字符的临时后缀值
for (i = patternlen - 2; i >= 0; --i)
{
//在上次匹配成功位置后,并且对应位置的临时后缀值不大于i到frontEnd的距离
if (i > frontEnd && mSuffer[patternlen - 1 - frontBegin + i] < i - frontEnd)
{
mSuffer[i] = mSuffer[patternlen - 1 - frontBegin + i];
}
else
{
//重新设置两个标志
frontBegin = i;
if (i < frontEnd)
{
frontEnd = i;
}
//常规扫描
while (frontEnd >= 0 && mPattern[frontEnd] == mPattern[patternlen - 1 - frontBegin + frontEnd])
{
frontEnd--;
}
mSuffer[i] = frontBegin - frontEnd;
}
}
}
//获得好后缀值
void BmPattern::getRigString()
{
int i;
int j;
int patternlen = mPattern.size();
//全部位置都默认设置为匹配模式长度
for (i = 0; i < patternlen; ++i)
{
mRigString[i] = patternlen;
}
//处理好后缀在匹配模式开头出开始,既是好后缀长度小于(patternLen - 坏字符处)
j = 0;
for (i = patternlen - 1; i >= 0; i--)
{
if (mSuffer[i] == i + 1)
{
for (; j < patternlen - 1 - i; j++)
{
if (mRigString[j] == patternlen)
{
mRigString[j] = patternlen - 1 - i;
}
}
}
}
//处理普通好后缀,既是好后缀长度为(patternLen - 坏字符处)
for (i = 0; i < patternlen - 1; i++)
{
mRigString[patternlen - 1 - mSuffer[i]] = patternlen - 1 - i;
}
}
//匹配文本类
class CheckText
{
public:
CheckText(); //默认构造函数
CheckText(string text); //字符串构造函数
void BmCheck(BmPattern pattern);//进行Boyer-Moore算法匹配模式
void outputResult(); //输出结果
private:
string mText; //需要匹配的文本
int matchedNum; //已经匹配到的数目
int matchedAddr[MAX_PATTEN]; //模拟栈,用来存放匹配到的地址
};
CheckText::CheckText()
{
matchedNum = 0;
//To be continue;
}
CheckText::CheckText(string text)
{
mText = text;
matchedNum = 0;
}
void CheckText::BmCheck(BmPattern pattern)
{
int scanBegin = 0; //进行模式匹配的起始位置,在Text中的扫描
int currentPoint; //匹配模式扫描指针,在pattern中扫描
int textlen = mText.size();
int patternlen = pattern.mPattern.size();
while (scanBegin <= textlen - patternlen)
{
currentPoint = patternlen - 1;
while (currentPoint >= 0 && pattern.mPattern[currentPoint] == mText[scanBegin + currentPoint])
{
currentPoint--;
}
if (currentPoint < 0)
{
matchedAddr[matchedNum++] = scanBegin;
scanBegin += pattern.mRigString[0];
}
else
{
scanBegin += max(pattern.mRigString[currentPoint], pattern.mMisChar[mText[scanBegin + currentPoint]] - patternlen + 1 + currentPoint);
}
}
}
void CheckText::outputResult()
{
if (0 == matchedNum)
{
cout << "No matched!" << endl;
}
else
{
cout << matchedNum << " Matched!" << endl;
for (int i = 0; i < matchedNum; i++)
{
cout << "\t" << i + 1 << " :\t" << matchedAddr[i] << endl;
}
cout << endl;
}
}
int main()
{
string virusString;
string sannerText;
while (true)
{
cout << "Input the virusString and the sannerText." << endl;
cout << "virusString:\t";
cin >> virusString;
cout << "scannerText:\t";
cin >> sannerText;
BmPattern virus(virusString);
CheckText text(sannerText);
text.BmCheck(virus);
text.outputResult();
}
return 0;
}