字符串匹配算法——BM算法
欢迎关注个人公众号:程序员宅急送
公众号微信号:Geek_Zhai
核心思想:
利用字符串的特点,跳过绝对不可能匹配的字符(多移动几个字符),提高效率。
算法讲解:
BM算法有两大规则:坏字符规则和好后缀规则
1、坏字符规则
图解如下
光有坏字符是不够的!因为坏字符可能会出bug,让你后退着滑动…
如下图
2、好后缀规则
鉴于坏字符规则可能出现的“倒滑”问题,我们需要好后缀规则
好后缀规则分为两部分——好后缀全匹配规则和好后缀的后缀子串全匹配模串前缀子串规则
首先,我们要知道什么叫好后缀,好后缀就是坏字符后面的那一串后缀字符串。
如下图
(1)好后缀全匹配规则
好后缀全匹配规则与坏字符规则有些类似,但是他匹配的是整个后缀字符串。
如下图
(2)好后缀的后缀子串匹配模串前缀子串规则
正因为可能因为找不到全匹配得好后缀导致得滑动过多,所以我们需要“好后缀的后缀子串匹配模串前缀子串规则”。
如下图
总结:
坏字符规则和好后缀规则的基本原理都讲完了,那在使用的时候怎么选择呢?
我们可以分别计算好后缀和坏字符向后滑动的位数,然后取两个数中最大的,最为模式串往后滑动的位数。这种处理方法可以避免我们前面说到的“坏字符规则向前滑”的现象。
代码如下:
class Solution
{
public:
Solution()
{
for(int i =0;i<26;++i)
{
wordIndex[i]=-1;
}
for(int i =0;i<10;++i)
{
suffix[i]=-1;
prefix[i]=-1;
}
}
void createWordIndex(const string& src,const string& dst)
{
int dstL = dst.size();
int srcL = src.size();
for(int i =0;i<dstL;++i)
{
int index = dst[i]-'a';
//记录字符在模串中的下标i
wordIndex[index]=i;
}
}
void createSuffixAndPrefix(const string& dst)
{
int dstL = dst.size();
for(int i=0;i<10;++i)
{
suffix[i]=-1;
prefix[i]=false;
}
for(int i=0;i<dstL-1;++i)
{
int j =i;
int k =0;//公共后缀子串长度
//这个循环的意思就是将两个子串从后往前进行匹配(因为是好后缀,所以从后往前)
/*
*a e f a e f
* 假设j=i=2;k=6-1-0;k=5
* 那就是从dst[2]==dst[5]一点一点向前匹配
* 分别是'f','e','a',匹配
*/
while(j>=0&&dst[j]==dst[dstL-1-k])
{
--j;
++k;
//此处由于最外层循环i
//所以suffix[k]会被相同长度的反复赋值,用于保证公共后缀子串在靠后的位置而不是开头
/*
*a b a e f a
* 在i=0时候,dst[0]==dst[5];suffix[1]==0;
* 在i=2时后,dst[2]==dst[5];suffix[1]==2;被重新赋值,保证靠后少滑动
*/
suffix[k]=j+1;
}
//如果j==-1,那么suffix[k]=0;这说明这一对匹配的字符串中的某个子串就是模串的前缀子串!!!
//下标从0开始不就是前缀子串吗?
//这个用于我们的'好后缀部分子串匹配规则'
if (j == -1) prefix[k] = true;
}
}
int moveByBadWord(char badCode,int badCodeIndex/*在模串中的下标*/)
{
//计算字符数组坏字符
int index = badCode-'a';
return badCodeIndex-wordIndex[index];
}
int moveByGoodRear(int badCodeIndex/*在模串中的下标*/,const string& src,const string& dst)
{
int dstL = dst.size();
//好后缀长度
int k =dstL-1-badCodeIndex;
if(suffix[k]!=-1) return badCodeIndex-suffix[k]+1;
for(int r=badCodeIndex+2;r<=dstL-1;++r)
{
if(prefix[dstL-r]==true)
{
return r;
}
}
return dstL;
}
int BM(const string& src,const string& dst)
{
//构建字符hash数组
createWordIndex(src,dst);
createSuffixAndPrefix(dst);
int dstL = dst.size();
int srcL = src.size();
int i =0;
while(i<=srcL-dstL)
{
int j;//用于匹配的字符串下标
for(j = dstL-1;j>=0;--j)
{
if(src[i+j]!=dst[j])break;
}
if(j<0)
{
return i;//匹配成功
}
//坏字符滑动距离
int badMoveL= moveByBadWord(src[i+j],j);
//好后缀滑动距离
int goodMoveL = 0;
//一定要有好后缀的情况下,才使用好后缀规则
//不然每次都会返回完整的模串长度
if(j<dstL-1)
{
goodMoveL =moveByGoodRear(j,src,dst);
}
i=i+max(badMoveL,goodMoveL);
}
return -1;
}
//此处假设我们是26个小写字母的字符串,我声明一个数组,方便我找字符在对应模串中的位置
int wordIndex[25];
//因为好后缀字符串本身也是模串的字符串!
//所以我们可以通过提前处理模串的后缀子串的匹配子串,定义一个匹配数组,方便我们在使用’好后缀全匹配规则‘的时候直接查找下标!
//此处我假设模串最长不过10
int suffix[10];
//prefix用于记录好后缀字符串的子串在模串的前缀子串中是否存在匹配的对象。
//在我们使用’好后缀部分子串匹配规则’的时候用。
bool prefix[10];
};