这两天在学习KMP算法,算法的原理部分这里就不再深究,可以点击此处
进入阮一峰的文章,我个人觉得这篇文章分析的很简单,思路很清楚。
这篇文章主要来学习一下next[]数组的求法,希望可以对一些人有帮助,也方便自己以后的巩固。
首先以字符串 =ababcc=为例手动求其next[]的值。
请牢牢记住next[]的值是最大公共前后缀的长度。
以下用P表示真前缀,K表示真后缀,J表示next[]值
a: P和K都为空 J=0;
ab: P{a}K{b} J=0;
aba:P{a,ab} K{a,ba} J=1;
abab: P{a,ab,aba} K {b,ab,bab} J=2;
ababc: P{a,ab,aba,abab} K {c,bc,abc.babc} J=0;
ababcc: P{a,ab,aba,abab,ababc} K {c,cc,bcc,abcc.babcc} J=0;
如下表所示:
str | a | b | a | b | c | c |
---|---|---|---|---|---|---|
next | 0 | 0 | 1 | 2 | 0 | 0 |
i | 0 | 1 | 2 | 3 | 4 | 5 |
你们也可以多写几串不用很长,观察他的next[]值。
**
不难发现,next[i]如果是大于0的值,那么next[i]必然等于next[i-1]+1
即next[i]值一次只能加1
可以理解为胖子是一口一口吃出来的,也就是说你正在求得next值和上一个next值存在着某种关系,所以我们设置一个变量存上一个next值,而上一个next值就是上一位置的最大相同前后缀长度。
接下来看下面几个个字符串末尾元素的最大相同前后缀长度
adasdasdsafasdase3f
这个你看第一眼就能说出为0;
acfgqqa
显然这个是1;
acfgqqac
这个是2;
总结一下你的思考方式(最好把思路写出来一会有用),我们要做的就是把我们思考的方式用程序语言写出来而已。
第一个串和第二个串为一组我们分析一下;
为什么我们看一眼第1个串就知道他是0呢?因为末尾元素不等于头元素。
然而仅仅是这样么?那如果出现第3个串 这样的情况呢?所以还有一个被忽略的隐性条件在限制,那就是串1末尾的前一元素next值为0然后我们才判断尾部元素和头部元素,才让我们很轻易地判断串1末尾元素的next[]值为0.
所以我们写的代码也应该有这两个步骤,先判断此刻元素前一个元素next值是否为0;
如果是,再判断此元素是否与头元素相同来确定其next[]值是否为1(next值一次只能+1)
如果不是,我们就要判断前一元素的最长公共前后缀的后一元素是否与当前元素相等,依次向前判断。(值得注意的是,因为是公共缀,所以该缀一定能在串的最前方找到,当缀长为0时我们比较的就是头元素啦)
再看第三个串我们是怎么得到其为2呢?正常情况下(没有串2的情况下)下我们需要列出他的前后缀比较,然而在我们已知串2末尾Next的情况下还会这样比较么?显然不会,我们只是往后挪了一位判断他的next值会不会+1而已,)因为我们已经得到上文的第一个结论了,next[]的值只会一次增加1,而kmp算法过程中你永远知道前一元素末尾的next[]值
(而此种情况就是紧接着上面,前一元素最大公共前后缀不为0的情况)
这样我们发现 无论哪种情况我们都要知道上一元素的next[]值,然后比较上一元素最大公共缀后的那个元素(即str[next[i-1]]),所以我们拿一个专门的变量存放它,当然不存放也行 接下来上代码讲解
总结一下大概就是我们一直从头部在找一个特殊的”子串“,这个子串必须是前边某一元素的最大公共前后缀,这样我们向后移动拼接后的就是一个新的最大公共前后缀,如果查不到,继续用这个思想前移直到找到,或者直到比较的位置为0。
举个实例吧
查末尾元素D的next[],
A | B | C | A | B | C | D |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
0 | 0 | 0 | 1 | 2 | 3 | ? |
因为前一元素的next是3,所以我们把比较量移到其后边就是3+1的位置
该位置为A,与D不相等,即拼接不成功。
接下来我们前边的某一元素就变成了了位置为3的C;而位置为3的C的
next值为0,我就判断0+1的位置是否与待查元素相等,不相等就是拼接失败
为0结束,相等就是+1;显然不等所以7位置的next值为0。
(如果C处的next的值不是0我们需要接着重复步骤,直到拼接成功,或者与位置0比较。)
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str ="ababcc";
int i;
int next[6];
int maxlen=0;//存放next[i-1]的值,即上一元素的最大公共前后缀的长度
next[0]=0;
for(i=1;i<6;i++)//i表示我们要求的位置的next值 因为next[0]已知我们从1开始。
{
while(str[i]!=str[maxlen]&&maxlen>0)/*此处即为上文讲解的:当前一元素的next值不为0时,判断是否能与前边所求的最大缀“接上”*/。
maxlen=next[maxlen-1];//接不上就依次向前逐步减少最大缀的值,直到为0,这条语句时kmp算法的核心。
if(str[i]==str[maxlen])//此处即为上文过程中判断相等后next值加1的过程不过为了表示方便都先赋值给了maxlen而已。
maxlen++;//next值加1;
next[i]=maxlen;//该轮结束确定next的值,进行下一位置next值的计算。
}
//下边为测试输出部分
for(i=0;i<6;i++)
cout<<next[i]<<" ";
return 0;
}
next数组可以改进,原因在于当出现类似于主串“aaabaaaab”和模式串“aaaab”进行匹配时,在第一次匹配过程中,当匹配到主串的第四个字符时失配,模式串开始进行四次回溯,这时由于模式串的前四个元素均相同,所以后面三次回溯是没有必要的。从而可以得到:若回溯过程中发现第 i 次回溯且失配的元素与下一次回溯的元素相同,那么此次回溯便可跳过(跳过的方法是将第 i + 1 次回溯的元素对应的next数组中的值赋给第 i 次(即此次)回溯的元素对应next数组中的位置),从而提高效率。当于第 i 次回溯的失配元素与 i + 1 次回溯的元素不同时,则直接按照原有的kmp算法把 i + 1 次回溯元素的位置赋给第 i 次(即此次)回溯的元素对应next数组中的位置即可,也就是说在确定next值时就先把因为相同元素的堆叠造成的next值增大的情况给复原
留个示例代码
在这里插入代码片void get_nextval(char *c, int *nextval, int c_len)
{
int i = 1, j = 0;
nextval[i] = 0;
while (i < c_len)
{
//j - 1是模式串前缀单个字符的下标,i - 1是模式串后缀单个字符的下标;
if (j == 0 || c[i - 1] == c[j - 1])
{
i++;
j++;
if (c[i - 1] == c[j - 1])
nextval[i] = nextval[j];
else if(c[i - 1] != c[j - 1])
nextval[i] = j;//表示若当前的字符对不匹配,则模式串应从第 j 个
//元素开始重新与当前字符匹配(注意此时 i 与 j 均已加一);
}
else
{
j = nextval[j];
}
}
}