KMP算法中最大公共前后缀长度的理解(逐步分析)

一、前言

        笔者在拜读了网上众多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;
}

五、参考文献

从头到尾彻底理解KMP(2014年8月22日版)_kmp算法 csdn-CSDN博客

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值