字符串匹配(Boyer-Mooer 算法)

14 篇文章 2 订阅

题目描述:

       给定一个目标串T和模式串P,要求寻找P第一次在T出现的位置,并返回其下标,匹配不到则返回-1。

1. 算法思想


       在当前用于查找子字符串的算法中,BM(Boyer-Moore)算法是当前有效且应用比较广的一中算法,各种文本编辑器的“查找”功能(Ctrl+F),大多采用Boyer-Moore算法。比KMP算法快3~5倍。
       Boyer-Moore算法不仅效率高,而且构思巧妙,容易理解。1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了这种算法。
       我们知道常规的字符串匹配算法是从左往右的,这也比较符合我们一贯的思维,但是BM算法是从右往左的。BM算法其实是对后缀蛮力匹配算法的改进。BM算法中模式串不在每次只移动一步,而是根据已经匹配的后缀信息,来判断移动的距离,通常80%左右能够移动模式串的长度,从而可以跳过大量不必须比较的字符,大大提高了查找效率。
       为了实现更快的移动模式串,BM定义了两个规则,坏后缀规则和好后缀规则。这两个规则分别计算我们能够向后移动模式串长度,分别记为shift(坏字符)和shift(好后缀),然后选取这两个规则中移动大的,作为我们真正移动的距离。下面分别介绍这两个规则:

坏字符规则:

       先来看如何根据坏字符来移动模式串,shift(坏字符)分为两种情况:

       1. 坏字符没出现在模式串中,这时可以把模式串移动到坏字符的下一个字符,继续比较,如下图:

       2.坏字符出现在模式串中,这时可以把模式串最右边出现的坏字符和母串的坏字符对齐,当然,这样可能造成模式串倒退移动,如下图:

       显然这种情况我们就不能使用坏字符规则,此时我们需要使用另一种规则,即好后缀规则。

       为了用代码来描述上述的两种情况,设计一个数组bmBc['k'],表示坏字符‘k’在模式串中出现的位置距离模式串末尾的最大长度,那么当遇到坏字符的时候,模式串可以移动距离为: shift(坏字符) = bmBc[T[i]]-(m-1-i)。如下图:

       实际上我们要为可能会出现的每个的字符都设定一个bmBc值,如果字符集很大的话(例如中文),那么数组的规模就会很大,一个可行的做法是使用map存放模式串中每种字符中最右的那个距离模式串末尾的长度,然后用一个专门的函数来查看键值对的值,如果存在就返回相应的值,不存在就返回模式串的长度。代码如下:

/*
* @brief 计算坏字符规则数组
*
* @param[in] pattern是模式串
* @param[out] bmBc是生成的坏字符规则数组,这里使用map实现更方便
* @return 无
*/
static void cal_BmBc(string pattern, map<string,int> &bmBc)
{
	for (int i = pattern.length()-1; i >= 0; --i)
	{
		map<string,int>::iterator itr = bmBc.find(pattern.substr(i,1));
		if (itr == bmBc.end())
		{
			bmBc.insert(pair<string,int>(pattern.substr(i,1),pattern.length()-i-1));
		}
	}
}

/*
* @brief 返回给定字符的坏字符数
*
* @param[in] c 给定的字符
* @param[in] bmBc 坏字符规则数组
* @param[in] plen 模式串的长度
* @return 相应的坏字符数
*/
static int getBmBc(string c, map<string,int> bmBc, int plen)
{
	 map<string,int>::iterator itr = bmBc.find(c);
	 if (itr != bmBc.end())
	 {
		 return bmBc[c];
	 }
	 else
	 {
		 return plen;
	 }
}

好后缀规则:

       shift(好后缀)分为三种情况:

       1.模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠左边的子串对齐。

       2.模式串中没有子串匹配上后后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可。
       3.模式串中没有子串匹配上后后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。此时,直接移动模式到好后缀的下一个字符。
       为了实现好后缀规则,需要定义一个数组suffix[],其中suffix[i] = s 表示以i为边界,与模式串后缀匹配的最大长度,如下图所示,用公式可以描述:满足P[i-s, i] == P[m-s, m]的最大长度s。

       构建suffix数组的代码如下:

/*
* @brief 计算suffix数组,其含义是其中suffix[i] = s 表示以i为边界,与模式串后缀匹配的最大长度,
*		 用公式可以描述:满足P[i-s, i] == P[m-s, m]的最大长度s。
*
* @param[in] pattern 模式串
* @param[out] 生成的suffix数组
* @return 无
*/
static void cal_suffix(string pattern, int suffix[])
{
	int plen = pattern.length();
	suffix[plen-1] = plen;
	for (int i = plen-1; i >= 0; --i)
	{
		int x = i;
		while (x >=0 && pattern[x] == pattern[plen-1-i+x])
		{
			--x;
		}
		suffix[i] = i-x;
	}
}


       有了suffix数组,就可以定义bmGs[]数组,bmGs[i] 表示遇到好后缀时,模式串应该移动的距离,其中i表示好后缀前面一个字符的位置(也就是坏字符的位置),构建bmGs数组分为三种情况,分别对应上述的移动模式串的三种情况:

       情况1:模式串中有子串匹配上好后缀

       情况2:模式串中没有子串匹配上好后缀,但找到一个最大前缀
       情况3:模式串中没有子串匹配上好后缀,但找不到一个最大前缀
       构建bmGs数组的代码如下:
/*
* @brief 计算好后缀规则数组
*
* @param[in] pattern 模式串
* @param[out] bmGs 生成的好后缀规则数组
* @return 无
*/
static void cal_BmGs(string pattern, int bmGs[])
{
	int plen = pattern.length();
	int *suffix = new int[plen];
	cal_suffix(pattern,suffix);

	//模式串中没有子串匹配上好后缀,也找不到一个最大前缀
	for (int i = 0; i < plen; ++i)
	{
		bmGs[i] = plen;
	}

	//模式串中没有子串匹配上好后缀,但找到一个最大前缀
	for (int i = plen-1; i>=0; --i)
	{
		if (suffix[i] == i+1)
		{
			for (int j = 0; j < plen-i-1; ++j)
			{
				if (bmGs[j] == plen)
				{
					bmGs[j] = plen - 1 - i;
				}
			}
		}
	}
	//模式串中有子串匹配上好后缀  
	for (int i = 0; i < plen - 1; ++i )
	{
		bmGs[plen-1-suffix[i]] = plen - 1 - i;
	}

	delete[] suffix;
}

       这一部分代码挺有讲究,写的很巧妙,这里谈谈我的理解。讲解代码时候是分为三种情况来说明的,其实第二种和第三种可以合并,因为第三种情况相当于与好后缀匹配的最长前缀长度为0。
       由于我们的目的是获得精确的bmGs[i],故而若一个字符同时符合上述三种情况中的几种,那么我们选取最小的bmGs[i]。比如当模式传中既有子串可以匹配上好后串,又有前缀可以匹配好后串的后串,那么此时我们应该按照前者来移动模式串,也就是bmGs[i]较小的那种情况。故而每次修改bmGs[i]都应该使其变小,记住这一点,很重要!
       而在这三种情况中第三种情况获得的bmGs[i]值大于第二种大于第一种。故而写代码的时候我们先计算第三种情况,再计算第二种情况,再计算第一种情况。为什么呢,因为对于同一个位置的多次修改只会使得bmGs[i]越来越小。
       代码15-18行对应了第三种情况,21-33行对于第二种情况,35-38对应第一种情况。
       第三种情况比较简单直接赋值模式串的长度plen,这里就不多提了。
       第二种情况有点意思,咱们细细的来品味一下。
       1. 为什么从后往前,也就是i从大到小?     
              原因在于如果i,j(i>j)位置同时满足第二种情况,那么m-1-i<m-1-j,而第27行代码保证了每个位置最多只能被修改一次,故而应该赋值为m-1-i,这也说明了为什么要从后往前计算。
       2. 第23行代码的意思是找到了合适的位置,为什么这么说呢?
              因为根据suffix的定义,我们知道x[i+1-suffix[i]…i]==x[m-1-siffix[i]…m-1],而suffix[i]==i+1,我们知道x[i+1-suffix[i]…i]=x[0,i],也就是前缀,满足第二种情况。
        3. 第25-31行就是在对满足第二种情况下的赋值了。第27行确保了每个位置最多只能被修改一次。
       第35-38行就是处理第一种情况了。为什么顺序从前到后呢,也就是i从小到大?
       原因在于如果suff[i]==suff[j],i<j,那么m-1-i>m-1-j,我们应该取后者作为bmGs[m - 1 - suff[i]]的值。

2. 代码

       预处理时间O(N + M^2)
       匹配时间复杂度O(N)
       完整代码如下:
#include <iostream>
#include <map>
#include <string>
using namespace std;

int max(int a,int b)
{
	return a>b? a:b;
}
/*
* @brief 计算坏字符规则数组
*
* @param[in] pattern是模式串
* @param[out] bmBc是生成的坏字符规则数组,这里使用map实现更方便
* @return 无
*/
static void cal_BmBc(string pattern, map<string,int> &bmBc)
{
	for (int i = pattern.length()-1; i >= 0; --i)
	{
		map<string,int>::iterator itr = bmBc.find(pattern.substr(i,1));
		if (itr == bmBc.end())
		{
			bmBc.insert(pair<string,int>(pattern.substr(i,1),pattern.length()-i-1));
		}
	}
}

/*
* @brief 返回给定字符的坏字符数
*
* @param[in] c 给定的字符
* @param[in] bmBc 坏字符规则数组
* @param[in] plen 模式串的长度
* @return 相应的坏字符数
*/
static int getBmBc(string c, map<string,int> bmBc, int plen)
{
	 map<string,int>::iterator itr = bmBc.find(c);
	 if (itr != bmBc.end())
	 {
		 return bmBc[c];
	 }
	 else
	 {
		 return plen;
	 }
}

/*
* @brief 计算suffix数组,其含义是其中suffix[i] = s 表示以i为边界,与模式串后缀匹配的最大长度,
*		 用公式可以描述:满足P[i-s, i] == P[m-s, m]的最大长度s。
*
* @param[in] pattern 模式串
* @param[out] 生成的suffix数组
* @return 无
*/
static void cal_suffix(string pattern, int suffix[])
{
	int plen = pattern.length();
	suffix[plen-1] = plen;
	for (int i = plen-1; i >= 0; --i)
	{
		int x = i;
		while (x >=0 && pattern[x] == pattern[plen-1-i+x])
		{
			--x;
		}
		suffix[i] = i-x;
	}
}

/*
* @brief 计算好后缀规则数组
*
* @param[in] pattern 模式串
* @param[out] bmGs 生成的好后缀规则数组
* @return 无
*/
static void cal_BmGs(string pattern, int bmGs[])
{
	int plen = pattern.length();
	int *suffix = new int[plen];
	cal_suffix(pattern,suffix);

	//模式串中没有子串匹配上好后缀,也找不到一个最大前缀
	for (int i = 0; i < plen; ++i)
	{
		bmGs[i] = plen;
	}

	//模式串中没有子串匹配上好后缀,但找到一个最大前缀
	for (int i = plen-1; i>=0; --i)
	{
		if (suffix[i] == i+1)
		{
			for (int j = 0; j < plen-i-1; ++j)
			{
				if (bmGs[j] == plen)
				{
					bmGs[j] = plen - 1 - i;
				}
			}
		}
	}
	//模式串中有子串匹配上好后缀  
	for (int i = 0; i < plen - 1; ++i )
	{
		bmGs[plen-1-suffix[i]] = plen - 1 - i;
	}

	delete[] suffix;
}

/*
* @brief BM算法的主逻辑,从模式串的最后一个字符往前匹配
*
* @param[in] haystack 目标串
* @param[in] needle 模式串
* @return 如果匹配成功则返回第一次出现的位置,否则返回-1
*/
int strStr(string haystack, string needle) {

	//异常处理
	if (haystack.length() == 0)
	{
		if (needle.length() == 0)
		{
			return 0;
		} 
		else
		{
			return -1;
		}
	}
	else if (haystack.length() < needle.length())
	{
		return -1;
	}

	map<string,int> bmBc; //相当于坏字符规则数组
	int plen = needle.length(); //模式串长度
	int *bmGs = new int[plen]; //好后缀数组
	cal_BmBc(needle,bmBc); //计算坏字符数组
	cal_BmGs(needle,bmGs); //计算好后缀数组


	int k = 0;
	int oldk = -1;//记录上一次匹配开始的位置
	while (k <= haystack.length() - plen)
	{
		int i = plen - 1;
		//匹配字符
		while (i>=0 && needle[i] == haystack[i+k])
		{
			--i;
		}
		//如果匹配成功,则返回下标
		if (i < 0)
		{
			//k += bmGs[0]; 
			delete[] bmGs;
			return k;
		}
		else
		{
			k += max(bmGs[i], getBmBc(haystack.substr(i+k,1),bmBc,plen)-plen+1+i);
			//如果这次匹配相对于上次匹配的位置没有变化,说明匹配失败
			if (k == oldk)
			{
				delete[] bmGs;
				return -1;
			}
			oldk = k;
		}
	}
	delete[] bmGs;
	return -1;
}

int main()
{
	//string str1="bbbwqerwqeqwrwqretreytruuuoipjkghfgsdfbnbnhgjdfgbababbbaabbba",str2="abb";
	//cout<<strStr(str1,str2);
	string s1,s2;
	cin>>s1>>s2;
	int res = strStr(s1,s2);
	if (res != -1)
	{
		cout<<"true"<<endl;
		cout<<res<<endl;
	} 
	else
	{
		cout<<"false"<<endl;
	}
	system("pause");
	return 0;
}

参考博客:http://www.cnblogs.com/xubenben/p/3359364.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值