题目描述:
给定一个目标串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.模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠左边的子串对齐。
![](https://i-blog.csdnimg.cn/blog_migrate/3da8bda529de809f6471fe6fb22cb95b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/bb5d702ac19794cf795a60aa148916aa.png)
![](https://i-blog.csdnimg.cn/blog_migrate/791b42c86a64d9389818059c1e426259.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c24f14711f5fccfcaf2a83cc5ba0370c.png)
构建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:模式串中有子串匹配上好后缀
![](https://i-blog.csdnimg.cn/blog_migrate/32fe0ff23ca3b2131f8b4e0e6bd09c68.png)
![](https://i-blog.csdnimg.cn/blog_migrate/18030c284e5cf53ac580cb5899e3485b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/95eacbe27c99333845686b0db0a04a3b.png)
/*
* @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)
完整代码如下:
#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