一、前言
笔者在拜读了网上众多KMP算法后,仍对部分细节存疑,前前后后花了两天时间去消化KMP算法的知识,又历时三天抽出时间整理得出本文。
个人认为,网上在讲KMP时并没有仔细探究最大公共前后缀长度的含义与求法,而是跳步到求next数组、nextval数组等等。虽然现在的我知道本质上是完全一样的,但是对于初学者来说着实不是很友好…
因此本文着重分析如何计算最大公共前后缀长度,在弄懂完这个之后再去理解next和KMPsearch就会觉得相对轻松。
若对你有帮助,欢迎点赞收藏~;若有问题,恳请诸位大佬在评论区指出,定会立即改正!
二、最大公共前后缀长度的理解
1. 一个串的前后缀:
前缀是指以串第一个字符开头且不包含最后一个元素的连续的子串,后缀是指以串最后一个字符结尾且不包含第一个元素的连续的子串(可以为串自身)。
eg.假设设串为ABCDEFGH,则前缀有A,AB,ABC等,后缀有H,GH,FGH等
2. 一个串的公共前后缀:
顾名思义,如果前缀集合与后缀集合中存在一个相同的元素,即存在一个子串,它既可以是前缀也可以是后缀,则称这个子串为该串的公共前后缀。
eg.假设串为AB_CSDN_AB,则公共前后缀有AB
3. 最大公共前后缀长度:
根据2中的分析我们可以得出一个串的公共前后缀,然而公共前后缀不一定只有一个,因此,需要我们寻找最长的公共前后缀,并得到它的长度,称为最大公共前后缀长度。
eg.假设串为ABA_CSDN_ABA,则公共前后缀有A,AB,即最大公共前后缀长度=3 (ABA)
4. 为什么要找最大公共前后缀长度:(逐步分析)
假设文本串是ABA_ABAY_CD,模式串是ABA_ABAX_CD
当我们匹配到文本串的Y,发现与模式串的X不匹配的时候,一般的暴力做法是:令模式串指回头部,令文本串指回原先的头部+1,再重新开始比较
首先我们要理解,匹配的本质是:刷新文本串的某一个位置(可以是一个字符,也可以是一个子串)来作为头部(该头部与模式串的头部相同),直到满足该头部接下来的元素与模式串头部接下来的元素相同
暴力做法是一个字符一个字符地作为头部来遍历文本串
但实际上,我们可以一个子串一个子串(这个子串也可以是一个字符)地作为头部来遍历文本串,这就是KMPSearch
在模式串的X位置之前的子串ABA_ABA中,我们可以找到最大公共前后缀ABA,它对应的长度是k=3,则代表模式串X位置之前的k个元素和模式串的前k个字符是完全相同的
(这里读者要留意一下“模式串X位置之前k个元素”和“模式串的前k个元素”的两个“前”的差别,相信你们可以get到)
而既然已经匹配到Y了,是不是说明文本串Y位置之前的k个元素 == 模式串X位置之前的k个元素 == 模式串的前k个元素
这说明什么?
相当于我们是不是已经找到了一个新的头部,这个头部不就是文本串Y位置之前的k个元素构成的子串,它正好对应模式串的头部
因此,按照先前讲的匹配的本质,我们只需要匹配头部之后的元素,即文本串从Y开始,亦即文本串的头指针不动,模式串从第k个元素开始,即模式串的头指针右移k位,然后再一个个字符判断
之后,我们只要重复上述思想,不断移动模式串的头指针,但保持文本串的头指针不动,即可实现KMP算法。因此我们需要找到模式串每一个位置的最大公共前后缀长度
三、计算最大公共前后缀的长度
1. 声明和初始化
令模式串为char p[length],声明int类型数组changdu[length]来存储不同位置对应的最大公共前后缀长度
声明两个变量i和k,i用来遍历模式串p,k用来存储最大公共前后缀长度
初始化changdu[0] = 0,因为只有一个字符时,一定没有公共前后缀,因此从i = 1,k=0开始遍历模式串p
每一次循环,刚开始的k是上一个位置对应的最大公共前后缀长度,而第k个位置即是公共前缀的下一个元素
2. p[i] = p[k]
若新添加的元素p[j]等于当前最大公共前缀的后一个元素即p[k],相当于此时新的后缀与新的前缀又相同了,所以最大公共前后缀多了一个元素,长度加1,即changdu[i] = ++k
3. p[i] != p[k]
若新添加的元素p[j]不等于当前最大公共前缀的后一个元素即p[k],则原先的公共前后缀被破坏,不再成立
所以,此时需要找到是否存在一个更短的公共前后缀,令原先的公共前后缀为子串s,而新的更短的公共前后缀实际上就是:s+p[i]的公共前后缀,即“公共前后缀的公共前后缀”
因此,更新k值为s的最大公共前后缀长度,即第k-1个位置的最大公共前后缀长度 = changdu[k-1]
4.赋值
因此既然每一次都是寻找公共前后缀,我们只需要不断递归上述步骤
当遇到了p[i] = p[k]的情况,才把当前的k值赋给changdu[i]
当k==0 || k==1时,不需要再继续递归,因为此时可以断定不存在公共前后缀,所以把k=0赋给changdu[i]
四、代码
1. GetChangdu函数
/*
1.k是当前子串的最大公共长度
2.changdu[i]存储的是当前子串0~i所对应的最大公共长度值k
综上,p[k]则是原本最大公共前后缀中的前缀的后一个元素
*/
void GetChangdu(char* p,int changdu[],int length)
{
int len = length;
changdu[0] = 0; //只有一个字符即p[0]时,最大公共长度当然为0
int i = 1,k = 0; //因此我们从p[1]开始遍历,并初始化最大公共长度为0
while (i < length){
//若新添加的元素p[j]等于当前最大公共前缀的后一个元素p[k]
//则相当于此时新的后缀与新的前缀又相同了
//因此直接将当前的k+1赋给changdu[i]即可
if (p[i] == p[k]){
changdu[i] = ++k;
i++;
}
//当k值不断缩小为公共前后缀的最大公共长度直到缩小到为1
//或者不存在公共前后缀时
//代表此时的串没有公共前后缀,故令changdu[i] = k = 0即可
else if (k == 0){
changdu[i] = k;
i++;
}
//若新添加的元素p[j]不等于当前最大公共前缀的后一个元素p[k],则新的后缀与新的前缀不同了
//此时需要找到是否存在一个更短的公共前后缀
//相当于是寻找(公共前后缀+新元素)的公共前后缀
//更新k值为最大公共前后缀的最大公共长度即changdu[k-1],而不再是当前串的最大公共长度
//重复上述流程,就可以找到changdu[i]
else
k = changdu[k-1];
}
}
2. GetNext函数:
因为进行KMPSearch时,进行到模式串p的位置i时,向右移动的距离实际上是changdu[i-1],因此我们只需要将原来的changdu数组右移,并赋值changdu[0] = -1即可
这就是网上大多方法实现next的基本思想
(其实不难得出,可以将GetChangdu和GetNext合二为一,具体的可以参考下面给出的链接,本文只专注于解析GetChangdu)
void GetNext(int *next,int *changdu,int length){
int temp[length];
for (int i=0;i<length;i++)
temp[i] = changdu[i];
for (int i=1;i<length;i++)
next[i] = temp[i-1];
next[0] = -1;
}
3. KMPSearch
int KMPSearch(char* maistr,char* substr,int length1,int length2,int* next){
int i=0,j=0;
while (i<length1 && j < length2)
if (j == -1 || maistr[i] == substr[j]){
i++;j++;
}
else
j = next[j];
if (j == length2)
return i-length2;
else
return -1;
}