来我们再聊聊 KMP 算法 -- 我懂,你也得懂

举报
看,未来 发表于 2020/12/29 23:27:46 2020/12/29
【摘要】 搞了一整天的KMP,自己动手写,先是感觉自己搞懂了,写完提交又崩溃了。反反复复一整天,刚刚总算是半抄半写过去了。 那现在,我就来看看自己能不能把这个算法讲清楚,当然,观众得有一定的基础,我语文不好,有的东西大家意会吧。 这篇不想用什么华丽的图片啊、辞藻啊堆砌,要堆砌上一篇已经堆砌过了,这篇更侧重于重难点突破。 写完语:我已经尽我所能让这篇不那么枯燥了,如果愿意看下...

搞了一整天的KMP,自己动手写,先是感觉自己搞懂了,写完提交又崩溃了。反反复复一整天,刚刚总算是半抄半写过去了。

那现在,我就来看看自己能不能把这个算法讲清楚,当然,观众得有一定的基础,我语文不好,有的东西大家意会吧。

这篇不想用什么华丽的图片啊、辞藻啊堆砌,要堆砌上一篇已经堆砌过了,这篇更侧重于重难点突破。

写完语:我已经尽我所能让这篇不那么枯燥了,如果愿意看下去,你可能会收藏起来。

为什么需要KMP算法

这个·不想多废话,它的时间复杂度是线性的。

KMP算法为什么快

暴力算法为什么慢

首先我要讲在前面,字符串匹配算法,不论是暴力破解,还是KMP这种高级算法,基础都是使用快慢指针的,如果对快慢指针不了解建议赶紧去刷题。

下面讲的,主串上的那个指针是慢指针,子串上那个是快指针。别纠结为什么叫快慢,一个名字而已。

暴力算法,在匹配失败之后,会将快慢指针都回溯。

KMP算法为什么快

因为它减少了对指针的回溯过程。

首先慢指针就不用回溯了!!!然后快指针也是回溯到半路。

在这里插入图片描述
你看像这样匹配失败了,慢指针就留在‘c’那里等着,然后快指针来回溯:
在这里插入图片描述
快指针也只是回溯到了‘b’。

为什么?上面那个可能不是很直观,还是要打个比方:
比方说对子串“ababc”,已经匹配到’c’失败了,不用回溯到开头,只需要回溯到第二个‘a’,因为在匹配失败前的两个字符肯定是‘ab’。

这样讲可能不是很严肃,但是应该比较好理解吧。那我总结了对这个点的话术,毕竟别人问你的时候你不能这么通俗的告诉人家。
我是这么想的:如果子串没有重叠部分,哪怕是一个字符的重叠,也没有,那么直接将快指针回溯到开头,然后将快指针对准慢指针。
如果子串有重叠的部分,就将快指针回溯到重叠部分后面那个位置,然后将快慢指针对准。
接着继续比对。

那么,你又要怎么知道,快指针应该回溯到哪里,说是重叠部分后面那个位置,那你每次匹配失败都要去找一下有哪里重叠?
那要是重叠部分很多,你要回到哪个?

这些准备工作,应该在进行KMP匹配之前做好。那就是next数组。

next数组

其实前面那些都是很基础的东西,不过这个能点进这篇的应该都是冲着next数组来的,而我的目的,也是要把这个数组的生成讲清楚。

相信代码大家都有,不过我依然要放在这里:

void getnext(string p, vector<int> &next)	//next在传入时应该进行扩容
{
	int len = p.size();
	int k = -1;
	int j = 0;
	next[0]=-1;

	while (j < len - 1)
	{
		if (k == -1 || p[k] == p[j])
		{ k++; j++; next[j] = k;
		}
		else { k = next[k];
		}
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

首先为了后面运算方便,将next[0]设置为-1,不得不说这个设置为-1非常之巧妙。

先不说巧妙在哪里,自己去写的话就知道了。
也先不说那个令人绞尽脑汁的 k = next[k],我们先把基础弄明白。

先看next[j] = k,这一句。

来我们来个简单的栗子:“ababcba”.

要对这个子串求它的next数组,是这样的。

1、a
2、ab
3、aba
4、abab
5、ababc
6、ababcb
7、ababcba

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

将这个字符串这样分一下,然后对号入座,看到我标的号了没,对应的是next数组中的号,最后那个可以去掉,因为如果整个串都对上了还回溯什么。

首先我们来看一下“前后子集“的概念,我自己起的名字,还不错吧。

拿4来说把,它的前子集有:

{
	a,
	ab,
	aba
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5

后子集有:

{
	b,
	ab,
	bab
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5

规律不难找啊。

那,他俩子集里面有一个同类,“ab”,将ab的长度填入next[4]里面。

接下来难度要稍微升级了。

这个next数组,也有半自动推导,碧如说4,它的对称度为2,那么如果在4的基础上,加上一个字符,这个字符刚好跟对称度+1的位置的字符对上,即如果加上的字符是a,那么便可以知道 5 的对称度为3,因为前面两个已经有 4 做了铺垫。
这就是:

if (k == -1 || p[k] == p[j])
		{ k++; j++; next[j] = k;
		}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这一个部分的原理。next[++j] = ++k;,是这样来的。

虽然我语文不好,但是讲到这个份上了,还不能心领神会那就不是我的问题了。

可惜,上面那个例子加上去的是 ‘c’。那就·是另外一部分代码的事情了:

else { k = next[k];
		}

  
 
  • 1
  • 2
  • 3

稍事休息。


k = next[k]

要理解这行代码,我们用另外一个字符串会比较直观一些。

“a b a b a b c b”

一步一步来啊,

1、
next[0] = -1;
k = -1,j=0; //a
k = 0,j=1;

next[1] = 0;
//这两个简直是铁索连环,就写一起吧

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
2、
j = 1,k = 0; //a,b
k = -1;
k = 0,j = 2;
next[2] = 0;

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
3、a,b,a
k = 1,j = 3;
next[3] = 1;

//看到了啊,出现了,进入if

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
4、a,b,a,b
k = 2,j = 4;
next[4] = 2;

//看啊,用到上面讲的了。
//其实还有一条铁律忘记说了,如果有耐心看到这里那我就说。后一位的对称度,顶多比前一位,多1!!!

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
5、a,b,a,b,a
k = 3,j = 5;
next[5] = 3;

//你去找,随便找,像我这么有耐心的真不多了。

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
6、a,b,a,b,a,b
k = 4,j = 6;
next[6] = 4;
//越来越接近目标了啊,马上就要断了香火了

  
 
  • 1
  • 2
  • 3
  • 4
7、a,b,a,b,*a*,b,c
k = 5,j = 7;
next[7] = 5?

这时候你会发现,它新加上来的那个字符,和对称度后面一位字符不匹配,‘c’!=‘a’!,那里我打了星标。

这时候怎么办?重头找?不可能的事,重头找的话,怎么说,那个代码该怎么写?一个一个在比对?

这时候还有另外一种想法,你看:
在插入‘c’之前,前面已经是对称的了有,好几组‘a,b’的存在。那么,为什么不推到当前失败‘a,b’的前面一个‘a,b’
去看看,这样既保证了对称度不会一下子跌到谷底,又能保证了对称性。因为第三个字符的前面也是‘a,b’,‘c’的前面也是‘a,b’,
那为什么不把这个对称轮回一轮一轮往前提并匹配呢?
如果最后真的轮回到了0点,那也总比直接回到原点有不知道后面会不会有惊喜要来的强一些。

那么,要怎么将快指针(k)回溯到前一个轮回的后一个字符呢?
其实上面跟大家开了个玩笑,哈哈,不知道有没有人发现。
k = 5?
这个k,7还是6?看看清楚,不记得的话把上面代码翻出来看看。

那么你看看 ,这时候的next[k]存的是什么东西,是不是上一轮的对称度,要是不记得,我给你找:`next[4] = 2`,那这个对称度是什么东西?
是不是等于字符串中上一轮轮回对应的后一个位置!注意,数组是以0为下标开始的!

是不是绕晕了?
我把一些概念再捋一下:


代码:
if (k == -1 || p[k] == p[j])
		{ k++; j++; next[j] = k;
		}
		else { k = next[k];
		} 对称度:最高有几个字符的相同子集。
轮回:比方说对称度为2的时候,‘a,b’为一个轮回,第一个‘a,b’为第一个轮回。
铁律:后一位的对称度,顶多比前一位,多1!!!
示例字符串:
“a b a b a b c b”

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

KMP匹配、

这个匹配就比较好理解了,该注释的地方我注释了

int kmp(string s, string p)
{
	int i = 0;
	int j = 0;
	int sLen = s.size();
	int pLen = p.size();

	if (pLen == 0 )
		return 0;

	vector<int> vec(pLen, 0);
	getnext(p,vec);	//获取next数组

	while (i < sLen && j < pLen)
	{
		if (j == -1 || s[i] == p[j])
		{ i++; j++;
		}
		else
		{ //②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]  //next[j]即为j所对应的next值  j = vec[j];
		}
	}
	if (j >= pLen)
		return(i - j);
	return -1;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

KMP算法整体实现(LeetCode测试通过)

#include<iostream>
#include<string>
#include<vector>

using namespace std;

void getnext(string p, vector<int> &next)	//next在传入时应该进行扩容
{
	int len = p.size();
	int k = -1;
	int j = 0;
	next[0]=-1;

	while (j < len - 1)
	{
		if (k == -1 || p[k] == p[j])
		{ k++; j++; next[j] = k;
		}
		else { k = next[k];
		}

	}

}


int kmp(string s, string p)
{
	int i = 0;
	int j = 0;
	int sLen = s.size();
	int pLen = p.size();

	if (pLen == 0 )
		return 0;

	vector<int> vec(pLen, 0);
	getnext(p,vec);	//获取next数组

	while (i < sLen && j < pLen)
	{
		if (j == -1 || s[i] == p[j])
		{ i++; j++;
		}
		else
		{ //②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]  //next[j]即为j所对应的next值  j = vec[j];
		}
	}
	if (j >= pLen)
		return(i - j);
	return -1;
}


int main()
{	
	vector<int> vec1(10,0);
	//for (int i = 0; i < vec1.size(); i++)
	//	cout << vec1[i] << " ";
	//cout << endl;

	string str = ""; string str2 = "";

	int a = kmp(str,str2);
	cout << a << endl;

	/*getnext(str2,vec1);

	for(int i = 0;i<vec1.size();i++)
		cout << vec1[i]<<" ";
	cout << endl;*/
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83

文章来源: lion-wu.blog.csdn.net,作者:看,未来,版权归原作者所有,如需转载,请联系作者。

原文链接:lion-wu.blog.csdn.net/article/details/105778889

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。