在一个较长的字符串中寻找一个较小的字符串,使用传统暴力求解,会多出很多不必要的计算步骤,而KMP算法可以有效解决这一点
目录
先看代码:
void getNext(int* next,string s)
{
int j=0;
next[0]=0;
for(int i=0;i<s.size();i++)
{
while(j>0&&s[i]!=s[j])
{
j=next[j-1];
}
if(s[i]==s[j])
{
j++;
next[i]=j;
}
}
}
int strSTr(string a,string b)
{
int next[b.size()];
getNext(next,b);
int j=0;
for(int i=0;i<b.size();i++)
{
while(j>0&&b[j]!=a[j])//这一步有点像递归
{
j=next[j-1];
}
if(a[j]==b[i])
{
j++;
}
if(j==b.size())
{
return (i-b.size()+1);
}
}
return -1;
}
介绍KMP算法
aabaabaaf
aabaaf
逐个匹配,匹配到f时,b与f不相等,此时看f前一位的next数组元素(next会在后面说明),并在子串中跳到对应数字位置,继续匹配。
在f前一位的next数组元素是f前面子串(aabaa)的最长前后缀,即2,这时候说明这个子串的开始两位与末尾两位一模一样,这说明了什么?在文本串中,我们匹配到了第六位' b ',' b '的前面就是这个子串("aabaa")的末尾"aa",我们刚刚通过next数组知道了"aabaa"的前两位与末尾两位相等,所以此时直接将aabaaf的第三位对齐aabaabaaf的第六位即可(因为末尾两位与开头两位相等,所以不用比较),为什么KMP不用再到模式串的起点那再次遍历呢,因为只有当子串的最后一位也匹配成功后,整个就算匹配完成,只有中途有一位匹配失败,则已经匹配了的全部前功尽弃没有作用,此时直接跟着后面匹配失败的那个位置重新匹配即可,但是不用重新匹配子串的全部,将相等的前后缀后一位开始匹配可以省不少步骤。
所以代码是这样的
for(int i=0;i<b.size();i++)
{
while(j>0&&b[j]!=a[j])
{
j=next[j-1];
}
if(a[j]==b[i])
{
j++;
}
if(j==b.size())
{
return (i-b.size()+1);
}
}
但是我们注意到,在匹配失败的时候,我们用了
while
,为什么呢?让我们思考一下,此时是不是跳转到了相同前缀的后一位进行比较,但是这个比较也可能不相等,如果不相等,那么就和我们当初模式串f与文本串的b发生冲突是一样的了。(有点像递归......)我们再往下面的代码看,如果匹配成功,那么
j++
,注意:j是对于子串而言的,j会随着匹配的进行,匹配成功则++
,匹配失败则跳到前面可以省略比较步骤的相同前后缀的后一位,从那一位接着开始比较,通过这样循环往复,最终当j等于子串的size()
后,即说明j是一路匹配成功来的,即在文本串中找到了与模式串相等的那一部分最后,我们
return
了(i-b.size()+1)
即子串在模式串相等部分的起始位置
理解next数组
next数组是相对于子串而言的,在实际比对过程中,aabaaf的任意一个位置都可能发生匹配冲突,而任意一个位置发生冲突后,我们都要去看他前一位的next数组的元素,什么意思呢?
aab的第三位b发生冲突,此时我们看前一位a的next数组,即aa的最长前后缀;
aaba的第四位a发生冲突,此时我们看前一位b的next数组,即aab的最长前后缀;
aabaa的第五位a发生冲突,此时我们看a的前一位(第四位)a的next数组,即aaba的最长前后缀。
所以在实际匹配过程中,任意一位都有可能发生冲突,我们都需要立刻知道其前一位的next数组,所以next数组需要提前把每一个子串的最长前后缀准备好
即我们要求
a
aa
aab
aaba
aabaa
aabaaf
的最长前后缀。
怎么求next数组?
void getNext(int* next,string s)
{
int j=0;
next[0]=0;
for(int i=1;i<s.size();i++)
{
while(j>0&&s[i]!=s[j])
{
j=next[j-1];
}
if(s[i]==s[j])
{
j++;
next[i]=j;
}
}
}
我们注意到这里有一个for循环,里面i在不断++,其中i每++一次,就确定next的一位,即确定next[i]的数值,
每个next[]在内部实现,(注意next[0]一定为0,因为a的前后缀为0,所以我们for循环直接从i=1开始)现在我们来具体分析一下内部:
首先在分析相等时的情况:
if(s[i]==s[j])
{
j++;
next[i]=j;
}
这里无需想太多,如果相等,则j++,说明此时在原有的基础上,又增加了一位相等的,就是说,如果之前j=1,有一位相等前后缀,现在又相等了,j++,即有两位相等前后缀了。
此时完成这个子串的next数组,储存他。
现在来分析一下不相等时:
while(j>0&&s[i]!=s[j])
{
j=next[j-1];
}
我们来看这三类数列
为什么不相等时j要跳转到next[j-1]呢?
我们知道j前面部分一定会等于j后面的一部分,即aba==aba,aba(前)有前后缀,前缀等于后缀,那么aba(后)就也有这个相同的前后缀,前缀等于后缀,也就是说aba(前)的前缀会等于aba(后)的后缀。
而j=next[j-1]会找到aba的相等前后缀。再由while判断是否相等,相等则继承在这之上的前后缀长度,并++;不相等则又进入while循环(有点像递归),继续找更小的。
谢谢。