绝对超级详细的kmp算法,next数组求法,以及回溯的理解

今天学习kmp算法,看了好多大佬的文章,总算是解决了理解层面上的问题,现在和大家分享一下。

kmp算法是什么

KMP算法是一种改进的字符串匹配算法
主串 为:a b a c a a b a c a b a c
模式串 为:a b a c a b
目的就是为了找出模式串主串中的起始位置
a b a c a a b a c a b a c 此时起始位置是6。
我们就是为了确定这个位置

为什么要用kmp算法

当然是为了更快找到这个位置,kmp的算法复杂度比较低,是一种优秀的算法。

一般匹配字符串时,我们从目标字符串str(假设长度为n)的第一个下标选取和ptr长度(长度为m)一样的子字符串进行比较,如果一样,就返回开始处的下标值,不一样,选取str下一个下标,同样选取长度为n的字符串进行比较,直到str的末尾(实际比较时,下标移动到n-m)。这样的时间复杂度是O(n*m)。

但是KMP算法:可以实现复杂度为O(m+n)!怎么样,是不是快了很多。

主要就是因为它充分利用了目标字符串ptr的性质(比如里面部分字符串的重复性,即使不存在重复字段,在比较时,实现最大的移动量)。

如何使用kmp算法

使用kmp算法有两个步骤

  1. 求出next数组
  2. 借助next数组使用kmp算法

步骤一、求next数组

next数组的含义就是一个固定字符串的最长前缀和最长后缀相同的长度
比如:

  • abcjkdabc,那么这个数组的最长前缀和最长后缀相同必然是abc。
  • cbcbc,最长前缀和最长后缀相同是cbc。
  • abcbc,最长前缀和最长后缀相同是不存在的。

注意最长前缀:是说以第一个字符开始,但是不包含最后一个字符。
比如aaaa相同的最长前缀和最长后缀是aaa。

对于目标字符串ptr,ababaca,长度是7

位置 next[0] next[1] next[2] next[3] next[4] next[5] next[6]
部分字符串 a ab aba abab ababa ababc ababca
长度 -1 -1 0 1 2 -1 0

(-1表示不存在,0表示长度为1,1表示长度为2,这样设置的原因是代码实现,写代码的时候方便)
下面是求next数组的代码实现

void cal_next(char *str, int *next, int len) { 
	next[0] = -1;//next[0]初始化为-1,-1表示不存在相同的最大前缀和最大后缀 
	int k = -1;//k初始化为-1 
	for (int q = 1; q <= len-1; q++) { 
		while (k > -1 && str[k + 1] != str[q]) {//如果下一个不同,那么k就变成next[k]
			 k = next[k];//往前回溯
		}
		if (str[k + 1] == str[q]){//如果相同,k++ 
			k = k + 1;
		} 
	next[q] = k;//这个是把算的k的值(就是相同的最大前缀和最大后缀长)赋给next[q] 
	}
}

解释代码

大家第一次看这个代码肯定一头雾水,稳住不要慌,相信自己肯定能弄懂!
首先函数声明,初始化,因为next[0]对应的就是"a",因为最长前缀以第一个字符开始,但是不包含最后一个字符,所以"a"没有最长前缀和最长后缀,因此对应的长度就是-1。

而后有一个for循环,就是遍历next数组。
再看for循环里面

while (k > -1 && str[k + 1] != str[q]) {
	k = next[k];
}
if (str[k + 1] == str[q]){
	k = k + 1;
} 

这里大家可能有点看不懂,下面用图讲解。
绝对超级详细的kmp算法,next数组求法,以及回溯的理解_第1张图片

首先因为k==-1,所以跳过while循环,又因为str[k + 1] != str[q],因此也不进入if语句,最后next[1]=-1。
绝对超级详细的kmp算法,next数组求法,以及回溯的理解_第2张图片
之后for循环q++,因为此时str[k + 1] == str[q],因此k++,最后next[2]=0,代表"aba"的最长前缀和最长后缀的相同长度为1。

为了更清晰,我们添加一个移动的ptr作比较
绝对超级详细的kmp算法,next数组求法,以及回溯的理解_第3张图片

之后for循环q++,因为此时str[k + 1] == str[q],因此k++,最后next[3]=1
绝对超级详细的kmp算法,next数组求法,以及回溯的理解_第4张图片
之后for循环q++,因为此时str[k + 1] == str[q],因此k++,最后next[4]=2
绝对超级详细的kmp算法,next数组求法,以及回溯的理解_第5张图片
之后for循环q++,因为此时str[k + 1] != str[q]

因此我们进入while循环
k=next[k]
我们称之为回溯,如何理解呢
我们现在面临的问题是,长度为4(k=3)的最长前缀和最长后缀不匹配,原因是末尾的str[k + 1] != str[q]
在这里插入图片描述
那么我们的解决办法是什么呢?
一般的思路:既然长度为4匹配不成功,那我们匹配长度为3不就行了?要是长度为3不行,再匹配长度为2。。。依次这样进行下去。
在这里插入图片描述
但是我们发现,长度为3的匹配出现了问题,不仅仅是末尾不匹配,连开头也不匹配了。我们不能依靠“只判断最后一位是否相同”这个条件来判断匹配是否成功了。
这样算法就会变得很复杂,就算最后一位相同了,我们还是要判断整体是否相同,变得更加麻烦了。
因此我们要确定正确的位置:
保证下一步匹配的前缀和后缀,只有最后一位不相同。

通过观察我们发现,虽然str[k + 1] != str[q],
但我们知道str[k + 1] 之前的序列,它的最长前缀和最长后缀相同的长度是已知的,例如
在这里插入图片描述
在c之前的序列,k=2(长度为3),那我们就可以用这个之前的序列。
在这里插入图片描述
我们发现这就是前面我们已经做过的步骤,
那我们要接着回溯下去,找这个最长前缀中的最长前缀,最长后缀中的最长后缀,也就是
在这里插入图片描述
也就是k=next[k],回溯
因为k=2,next[2]=0,因此k=0
此时str[k + 1]='b’和 str[q]=‘c’,发现str[k + 1] != str[q]
再次while循环,
k=next[k],回溯
因为k=2,next[0]=-1,因此k=-1
在这里插入图片描述
放大看一下
绝对超级详细的kmp算法,next数组求法,以及回溯的理解_第6张图片
此时k=-1,跳过while循环,
又因为str[k + 1] != str[q]
因此next[5]=-1。
绝对超级详细的kmp算法,next数组求法,以及回溯的理解_第7张图片
而后q++,因为str[k + 1] == str[q],因此k++,最后next[6]=0
循环结束,next数组创建完成。

以上大家应该能够理解next数组的代码思路了吧

步骤二、借助next数组使用kmp算法

之后的算法其实和求next数组差不多,本来想写一下,发现这篇文章写的很好,完整算法看这位大佬的就好了。

。。。思考了一下,还是自己再梳理一遍,也不是很难
首先这里默认已经求出了next数组

贴上代码

int KMP(char *str, int slen, char *ptr, int plen)
{
	int *next = new int[plen];
	cal_next(ptr, next, plen);//计算next数组
	int k = -1;
	for (int i = 0; i < slen; i++)
	{
		while (k >-1&& ptr[k + 1] != str[i])//ptr和str不匹配,且k>-1(表示ptr和str有部分匹配
			k = next[k];//往前回溯
    	if (ptr[k + 1] == str[i])
        	k = k + 1;
    	if (k == plen-1)//说明k移动到ptr的最末端,匹配成功了!
    	{
        	return i-plen+1;//返回相应的位置
    	}
	}
	return -1;  //匹配失败了,没有匹配到合适的
}

首先第一眼看这个代码,感觉和next数组的求法大同小异。

str是母串,ptr是子串。

主要就是从这个for循环开始,我们要明确k和i的含义

  • i:母串中的具体位置,从头开始依次扫描
  • k:从头开始,已经和母串匹配成功的子串中的连续的字符个数(0代表匹配成功1个,1代表匹配成功2个……)

所以刚开始一个都没有匹配的时候,k的值是不变的,但是i的值却在增加,图解说明一下

i=0,k=-1

a a a b c b d
b c b s

i=1,k=-1

a a a b c b d
b c b s

i=2,k=-1

a a a b c b d
b c b s

i=3,此时匹配到了一个,进入if结构,最后k=0

a a a b c b d
b c b s

i=4,此时匹配到了2个,k=1

a a a b c b d
b c b s

i=5,此时匹配到了3个,k=2

a a a b c b d
b c b s

i=6,这个时候出现了不匹配的情况

执行k=next[k],k=next[2],因为bcbs的next[2]值是0(一个),所以k=0,i=6图解一下就是

a a a b c b d
b c b s

转变为

a a a b c b d
. . b c b s

仍处于while循环中,还是没有匹配,所以k=next[0]
因此k=-1,q=6

a a a b c b d
. . . b c b s

还是没有匹配到,此时k=-1跳出循环,所以匹配结束,返回-1。

经过这样的梳理,大家应该对这个kmp算法完全理解了吧,欢迎在评论区进行讨论,共同学习进步。

你可能感兴趣的:(学到了新姿势)