KMP匹配算法的改进及代码实现(nextval)
前提
此文已假设你懂得了KMP的原理和next数组的求法,此处我对nextval(KMP改进)数组的原理进行介绍。
若没有相应基础的同学可以查看下方链接,先学习KMP基本原理,以及next数组的原理和代码实现。
1.KMP算法图解
2.为什么KMP算法不会跳过(漏掉)正确的答案
3.KMP算法之求next数组代码讲解
4.KMP算法之NEXT数组代码原理分析
看完上面的博客和视频,应该就能懂的KMP原理以及next数组的代码实现了(毕竟我就是这么过来的 T.T)。
KMP由于不用主串下标i的回溯,其时间复杂度由暴力匹配的O(m*n)改进到了O(m+n),每次失配,i呆在原地,j回溯到next[j]的位置就可以重新进行匹配了,十分方便。
但是,后面有人发现,KMP算法还是有缺陷——j回溯的还不够快
。因为一旦发现失配重新进行匹配时,j回溯的越多,匹配的就越快。例如,当主串S=“aaaabcde”,子串T=“aaaaax”,其next数组为012345。
在i=5,j=5时发生失配如下图①;于是j=next[5]=4,重新匹配如下图②;依然发现‘b’与子串4位置上的‘a’不相等,于是再重新匹配,j=next[4]=3,如下图③,这样依次类推是④⑤。直到j=next[1]=0时,根据next代码,此时i++,j++。得到i=6,j=1如下图⑥。
按照next数组进行回溯的方法,上面的②③④⑤其实是多余的判断,这是因为在T[5]失配,而T[5]又与前四位相等,既然S[5]!=T[5],那必然不会等于T[1]、T[2]、T[3]和T[4],之后的四次重新匹配也肯定是徒劳无功。
于是要重新设置一个新数组nextval
(默认nextval[1]=0,nextval[2]=1)能够使j跳过一些无意义的回溯,而快速回溯到能够正确开始匹配的位置。
思路
假设
此时在i处已经发生失配,j回溯到了next[j]的位置。
-
如果 T[i] != T[j] ,因为S[i]已经不等于T[i]了,那么S[i] 就可能会等于T[j],于是nextval[i]=j;(此时nextval数组和next数组的值是一样的)
-
如果T[j] == T[i],那么失配的S[i]必不会等于T[j]
于是我们就继续往前找前缀 j=next[j](如果不知道为什么找的是前缀可以查看KMP算法之求next数组代码讲解14分11秒),如果T[j]==T[i],就继续找(不然匹配始终会在主串i处终止,因为S[i]必不会等于T[j]),直到T[i] != T[j]或者 j ==0为止。
此时的nextval[i]=j,如果上述步骤使用循环语句,事实上也有nextval[i]=nextval[next[i]]。
手算nextval数组见数据结构 KMP改进算法 nextval数组求值
代码
//nextval数组
void get_nextval(char T[], int nextval[])
{
int i = 1;//后缀
int j = 0;//前缀
nextval[1] = 0;
while (i < T[0])
{
if (j == 0 || T[i] == T[j])
{
i++, j++;
if (T[i] == T[j])//多了判断
{
nextval[i] = nextval[j];
}
else
{
nextval[i] = j;
}
}
else
{
j = nextval[j];//继续往前找前缀
}
}
}
//next数组 作为参照不同处
void get_next(char T[], int next[])
{
int i=1;//后缀的最后一位的下标
int j = 0;//前缀的最后一位下标
next[1] = 0;//规定next[1]=0
while (i < T[0])//T[0]等于T串的长度,循环遍历T串,得到next表
{
if (j == 0 || T[i] == T[j])//如果前缀和后缀相等,找到一位next[i]
{
i++, j++;
next[i] = j;
}
else
{
j = next[j];
}
}
}
利用下面例子做一回实验
串第一位存放整个字符串的长度。分别使用next和nextval数组,每次回溯就计数一次
使用next数组:
使用nextval数组:
青山不改 绿水长流