Boyer-Moore算法实现类——简单封装了下

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:模式串中有子串和好后缀安全匹配,则将最靠右的那个子串移动到好后缀的位置。继续进行匹配。(继续进行匹配的意思是从新从尾开始匹配。可以优化下,既是判断找到的后缀的左边(前面)字符是否等于坏字符,如果是的话,则该后缀不符合要求,继续向前找,这样可以扩大移动距离。不过这样就不能实现匹配模式和被扫描字符串的分离了,因为要考虑被扫描的字符串。)

wps_clip_image-979

Case2:如果不存在和好后缀完全匹配的子串,则在好后缀中找到具有如下特征的最长子串,使得P[m-s…m]=P[0…s]。说不清楚的看图。(也可以参考下下面的求BmGs的Case2的那张图。)

wps_clip_image-1152

坏字符算法

当出现一个坏字符时, BM算法向右移动模式串, 让模式串中最靠右的对应字符与坏字符相对,然后继续匹配。坏字符算法也有两种情况。

Case1:模式串中有对应的坏字符时,见图。 
wps_clip_image-1349

Case2:模式串中不存在坏字符。见图。

wps_clip_image-1472

(到这里该思考下,如果坏字符是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(模式串)。

wps_clip_image-1885

(这里就关联一下上面提到的,k是模式串的最后一个字符的情况下,是不是还是这样处理了呢?需要分两种情况讨论下比较容易理解。

首先,该字符只在模式串中出现一次,那么这个BmBc值就该赋值为模式串的长度,为什么?可以参考下上面提到的坏字符算法的case2,既是虽然遇到了k这个坏字符,但是因为该模式串中不可能再有其它的k字符,所以可以向后移动patternLen个长度。

其次,也是另一种吧,就是在其它位置还重复出现了该字符。那么,就可以按照上面的定义来处理。

结论,在循环的时候,可以把最后一次字符的处理给忽略掉。

BM算法子串比较失配时,按好后缀算法计算模式串需要向右移动的距离,要借助BmGs数组。

BmGs数组的下标是数字,表示字符在模式串中位置。

BmGs数组的定义,分三种情况。

(注意i不属于好后缀,i对应被扫描的坏字符)

1、 对应好后缀算法case1:如下图:i是好后缀之前的那个位置。

wps_clip_image-2036

2、 对应好后缀算法case2:如下图所示:

wps_clip_image-2086

3、 当都不匹配时,BmGs[i] = strlen(模式串)

wps_clip_image-2145

在计算BmGc数组时,为提高效率,先计算辅助数组Suff。

Suff数组的定义:suff[i] = 以i为边界, 与模式串后缀匹配的最大长度,即P[i-s...i]=P[m-s…m]如下图:

wps_clip_image-2272

举例如下:

(这里遇到G为2应该就不会再问为什么不是为0了吧)

wps_clip_image-2380

分析

用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;

如下图所示:

wps_clip_image-2697

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]就没有直接关系了。(这里有错误,不是等于,是大于或等于)

wps_clip_image-3177


自己包装了下,并且填了点注释的代码如下:(重要函数只有四五个……)

#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;
}	


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值