前言
在学习kmp算法的时候只是一知半解,本来寻思会用就行了,但是刷题的时候经常报数组越界,想看别的同学用不同形式的next数组又不懂了,于是不得不把整个next数组的三种定义形式系统地都看一遍,以加深对kmp的理解.本文着重对比三种定义形式的区别,如果你对kmp算法本身有疑问推荐观看 链接: link
b站有个up搬的油管上的,讲的挺好
另外推荐阅读胡凡老师的<<算法笔记>>的讲解,很清晰也很详细,然后读罗永军老师的<<算法竞赛>>的讲解,如果能看懂,那你就不用看本文了.
第一种
算法笔记对next的定义是next[i]为s[0,i]的最长公共前后缀,前缀的最后一位的下标
- s是字符串,长度为len,下标0到len-1
- next的长度也为len,下标也是0-len-1;next[n-1]就是p[0,n-1]最长公共前后缀的下标
代码
void getNext(string p,int len){
next[0]=-1;//最长公共前后缀不能是字符串本身,p[0]的最长公前后缀长度为0,所以next赋值为-1
int j=-1;
for(int i=1;i<len;i++){//求解next[1]~next[len-1]
while(j!=-1&&p[i]!=p[j+1]){//j是最长公共前后缀最后一位的下标,i指向当前失配的字符,所以要用[j+1]去匹配[i]
j=next[j];
}
if(p[i]==p[j+1])j++;
next[i]=j;
}
}
可以看出,之所以把next[0]定义为-1不是-2或者其他值,是为了在匹配的时候对于下标j=-1的时候,依然可以通过索引next[j+1]访问到next[0]
第二种
在算法笔记定义的基础上,给next数组整体+1;此时 next[i]表示[0,i]最长公共前后缀长度 ,也是也是最长公共前后缀的前缀的下一个位置,所以在匹配的时候不用j+1.只要把上面代码小改一下就行
代码
void getNext(string p,int len){
next[0]=0;//最长公共前后缀不能是字符串本身,p[0]的最长公前后缀长度为0,所以next赋值为0
int j=0;//j的初值也要改
for(int i=1;i<len;i++){//求解next[1]~next[len-1]
while(j!=0&&p[i]!=p[j]){//用j匹配i就可以了
j=next[j];
}
if(p[i]==p[j])j++;
next[i]=j;
}
}
第三种
<<算法竞赛>>中的定义,在第二种定义的基础上,把数组整体向高位移动一个位置即 next[i]表示[0,i-1]区间的最长公共前后缀的长度,也就是前缀的下一个位置
注意
- p的区间为[0,len-1],长度为len
- next的区间为[0,len],长度为len+1,最后一个元素是next[len],表示字符串p整个的最长公共前后缀的长度.要把next设置得长一些以防下标越界
void getNext(string p,int len){//len是数组p的长度,是next的最后一个元素的下标,next比p长1
next[0]=0;
next[1]=0;
//理论上next[0]是用不到的
int j=0;
for(int i=1;i<len;i++){
while(j&&p[i]!=p[j]){
j=next[j];
}
if(p[i]==p[j])j++;
next[i+1]=0;//求p[0,i]的最长公共前后缀长度
}
}
注意for循环的终止条件和上面的两种定义一样,next偏移的原因是在给next赋值的时候赋值给了next[i+1]而不是next[i]
对比
项目 | 第一种 | 第二种 | 第三种 |
---|---|---|---|
p长度 | len | len | len |
next长度 | len | len | len+1 |
next[i]对应p的区间 | [0,i] | [0,i] | [0,i-1] |
next值的含义 | 最长公共前后缀,前缀的最后一个元素的下标 | 最长公共前后缀长度 | 最长公共前后缀长度 |
当前字符匹配成功的判断条件 | p[i]==p[j+1] | p[i]==p[j] | p[i]==p[j] |
初始化 | j=-1,next[0]=-1 | j=0,next[0]=0 | j=0,next[0]=next[1]=0 |
如果p的下标从1开始呢?
在刷题的时候,有时由于各种原因,数组p的戏下标区间为[1,len],长度为len,那么next数组的定义应该有什么变化呢?
最直接的感觉是:p数组的下标整体+1,用第二种定义应该很简单地套用
我们定义next[i]为p[1,i]最长公共前后缀的长度,也是前缀最后一个元素的下标
void getNext(string p,int len){
next[1]=0;//最长公共前后缀不能是字符串本身,p[1]的最长公前后缀长度为0,所以next赋值为0
int j=0;//j的初值也要改
for(int i=2;i<=len;i++){//求解next[2]~next[len]
while(j!=0&&p[i]!=p[j+1]){
j=next[j];
}
if(p[i]==p[j+1])j++;
next[i]=j;
}
}
观察本题中next的定义,发现它有貌似结合了第一种定义与第二种定义,既表示最长公共前后缀的长度,也表示前缀最后一个元素的下标
如果从第二种定义(最长公共前后缀的长度)的角度来理解:原本第二种定义种,next[i]是p[0,i]最长公共前后缀的长度,前缀的区间为[0,next[i]-1],所以next[i]就是最后那个元素的下一个元素的下标.但是在现在这个问题中,由于p数组整体下标+1,所以第二种定义next[i]仍然表示p[1,i]最长公共前后缀的长度前缀的区间为[1,next[i]],所以next[i]是前缀最后一个元素的下标.也就是说,next第二种定义相比于第一种定义的"+1"的特性由于p也整体下标+1抵消了,所以当前字符匹配的条件仍然是p[i]==p[i+1],使得代码看起来和第一种定义很像.由于p地下标整体+1,所以要初始化next[1]而不是next[0]
如果从第一种定义的角度来看,next[0]原本初始化为-1,由于next[0]不用了,所以初始化next[1].如果next[1]初始化为-1,那么next[1]+1就不能正确地找到p[1],而是找到p[0].所以next[1]要初始化为0,同理j也要初始化为0;
絮絮叨叨
个人感觉第二种定义是最简洁地也是最好理解的,就采用字节擅长的定义去写就行,至少这样以后看到了其他同学写的kmp也能快速地把思想转换过来了.
作者还在学习中,如果本文某处有bug,欢迎指出.