KMP算法精讲

今天被KMP算法可算好好折磨了一番,看了好几个教程,总感觉自己被卡在某一步不能理解过去,比如为什么这一步要这么做啊,这么做为什么就可以达到想要的效果啊之类的,于是乎,在今天上午下课的某个课间,仔细静下心来看了看,想了想,突然醒悟了这关键的一步,然后再理解起整个kmp算法就变得容易了起来.

所以,我相信有很多和我一样被这样困在kmp算法里的,所以想写一篇文章来记录一下自己的理解,以便他人参照和自己复习.

本章笔者将以自己学习kmp时的顺序加上个人的理解来讲解它.

各个模块之间都有关联,所以从头开始阅读效果可能更佳.

目录

什么是前缀表

字符串不匹配时,我们怎么知道要回退到哪个位置?

最长公共前后缀(前缀,后缀理解)

为什么前缀表能告诉 我们匹配不成功时要跳转的位置呢?

如何求解前缀表(next数组)

构造next数组(代码实现)

1.初始化

2.处理前后缀不相同的情况

3.处理前后缀相同的情况

利用next数组来匹配

kmp总代码


首先,kmp算法主要应用于字符串匹配的场景中.它的主要思想是:当出现字符串不匹配的情况时,可以根据之前已经匹配的文本内容,找到下次重新匹配时的位置,从而避免每次都从头再去匹配.

它和暴力法的主要区别就是:kmp算法记录了已经匹配的文本内容,而暴力法只是每次重新从头匹配,而不会记录已经匹配过的文本内容.

所以kmp算法的核心是:在匹配中,如何记录已经匹配的文本内容.也是next数组所担负的“重任”,next数组本质就是一个前缀表.

什么是前缀表

代码中所使用的next数组就是一个前缀表.

作用是:当文本串模式串不匹配的时候,模式串应该从哪个位置开始匹配. 位置肯定是前面的,因为当前位置已经不匹配了,不可能是后面的,所以说前缀表是用来回退的.

上面说到了文本串和模式串,这里说一下分别代表什么:

例如在文本串”aabaabaafa“中查找是否出现过模式串”aabaaf

文本串相当于那个主串,模式串是需要被查找的串.

这里说一下,前缀表的作用,具体为什么后面再讲.

如上,在文本串aabaabaafa”中查找是否出现过模式串”aabaaf“.

至于为什么回退到b位置,后面会说.

 回退了之后u,从回退的位置继续和文本串原来的位置继续比较.

这样就完成了字符串的匹配操作.

这里面涉及到了两个问题:

1.当字符串不匹配时,我们怎么知道要回退到那个位置?

2.为什么回退到这个位置?【这个是当时笔者不理解的地方所在,后面讲一下自己的看法】

 先来回复第一个问题:

字符串不匹配时,我们怎么知道要回退到哪个位置?

至于回退到哪个位置就是前缀表的任务所在了.

它的任务是:当匹配失败时,找到之前已经匹配好的位置,然后再重新匹配.

这意味着在某个字符匹配失败时,前缀表会告诉你下一步匹配时,模式串该跳到哪个位置重新开始匹配.

这次再来回答一下什么是前缀表

记录下标i之前(包括i)的字符串(模式串)中,有多大长度的相同前缀后缀(最长公共前后缀).

比如模式串”aabaaf“,当i=2时,前缀表中next[2]记录的是”aab“的相同前缀后缀,因为b的下标是2.要记录下标为2之前的相同前缀后缀。此时没有相同的前后缀,即为0,next[2] = 0.

至于怎么求,下面马上讲解.

最长公共前后缀(前缀,后缀理解)

前缀,后缀这两个概念一定要理清楚!一定要!

当初笔者学习的时候,感觉自己掌握了前缀后缀,结果没想到卡着的地方正是这里。所以一定要明白!

前缀

字符串的前缀是指不包含最后一个字符所有第一个字符开头的连续子串.

例如字符串“aabaaf”.

它的前缀有:a,aa,aab,aaba,aabaa.

它总是以第一个字符开头,只要不包含最后一个字符,都是前缀串.

如下图,可体验变化



后缀 

字符串的后缀是指不包含第一个字符所有最后一个字符结尾的连续子串.

还是例如字符串“aabaaf”.

它的后缀是f,af,aaf,baaf,abaaf.

请仔细记住每一个细节,正确的理解对后面的内容很重要!

说到这里,那到底什么是相同的前缀后缀(最长公共前后缀)呢?

前缀表要求的就是相同的前缀后缀.

有了以上讲的,大家对这个应该也会有些理解.

就是前缀和后缀相同的字符.

例如:字符串“aabaa”.

第一步开始:前缀为“a”,后缀也为“a”,所以此时有了一个相同的前缀后缀

但是前缀后缀不只有一个,所以我们继续向下看

第二步,前缀为“aa”,后缀也为“aa”,此时有了两个字符相同,即有了两个相同的前缀后缀.

接着,第三步,前缀为“aab”,后缀为“baa”.此时便不一样了,到此为止.

所以字符串"aabaa"相同的前缀后缀是“aa”,长度为2.

再举一个例子,比如字符串“abab”.

第一步,前缀为“a”,后缀为“b”,直接不相等,所以此时它们没有相同前缀后缀,长度为0.

知道了什么是相同前缀后缀,那么next数组我们也就会求了,就是相同前后前后缀的长度.

下面就是重点了:

为什么前缀表能告诉 我们匹配不成功时要跳转的位置呢?

先来回顾一下刚才的匹配时的情况:

文本串aabaabaafa”中查找是否出现过模式串”aabaaf“.

当到第5个位置发现不匹配了.

于是退回到2的位置.

然后继续进行与文本串匹配.但是为什么是退回2的位置,而不是3,4或者别的位置呢(为什么前缀表可以告诉我们匹配失败之后跳到哪个位置)?

 下面这句话对于理解它非常重要:

下标5之前的这部分字符串(“aabaa”)的最长相等的前缀 和 最后自字符串是 子字符串“aa”.

因为他们是相等的前缀后缀,而我们匹配失败的位置是后缀子串的后面,所以我们找到与其相同的前缀后面重新匹配就行了.

这是理解整个next数组的精华!

可以举个比喻来说明:前缀就是我们的后备能源,后缀就是我们现在使用的能源(即正在比较中的字符串),如果现在的能源的下一个字符不能符合我们的需求了(b和f不匹配了),那我们就启用后备能源(后备能源等于现在使用的能源,相当于我们查找的相同前缀后缀,只是两个能源的后面一个字符不相等,所以跳到这个不相等的位置继续与文本串比较,而这个字符之前的能源是完全一样的),即跳到前缀串的后面的位置,然后再次进行比较即可.(如果依然不相等则继续向前回退).

我用这个例子来套用上面所说的:

文本串aabaabaafa”中查找是否出现过模式串”aabaaf“.

后备能源是前缀(aa【这个aa是aabaaf前面的两个aa】),后缀是现在使用的(aa【这个aa是aabaaf后面的两个aa】),现在使用能源aa下一个字符f与我文本串的b不匹配了,那我们启用后备能源(aa),这个后备能源与现在能源完全一样[aa],只是后面的一个字符不一样了,现在能源后面的字符是f,而后备能源后面一个字符是b,即跳到b这个位置,再次比较,发现b和b匹配上了,

继续向后,a和a,a和a,f和f都成功匹配上了,所以此时就已经找到了模式串.

理解了这一些,KMP算法你就成功理解了90%了.

接下来就该求解前缀表了,即next数组了

如何求解前缀表(next数组)

根据上面的理解,我们也知道了

next数组里的每个值,对应的是下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀.

接着看例子:

文本串aabaabaafa”中查找是否出现过模式串”aabaaf“.

我们之前所讲的next数组,只与模式串有关系,和原文本串没有任何联系。包括动的时候,只有模式串中在回退,文本串中不变,一直向前走.

所以在模式串“aabaaf”中:

下标i = 0时,此时字符串为“a”它既不是前缀也不是后缀(前缀是不包括最后一个字符的所有以第一个字符开头的字符串,后缀是不包括第一个字符的所有以最后一个字符结尾的串).即相同前缀后缀长度为0.next[0] = 0.

下标i = 1时,此时字符串为“aa”,前缀为“a”,后缀也为“a”,所以相同前缀后缀长度为1,next[1]

=1.

下标i=2时,此时字符串为“aab”,前缀有两个“a”,“aa”,后缀也有两个“b”,“ab”,分别比较看是否相等,可以发现“a”不等于“b”,这里不相等,后面就更不可能相等了,所以相同前缀后缀长度为0,next[2] = 0.

以此类推,下标i=3时,相同前后缀长度为1,next[3]= 1

next[4] = 2,next[5] = 0.

所以这样我们就求解了next数组:next=[0,1,0,1,2,0].

至于回退的规则,规则是当遇到不匹配的位置时,我们要看当前位置的前一个字符的前缀表的数值是多少,然后回退到对应位置.

还是那个例子:

文本串aabaabaafa”中查找是否出现过模式串”aabaaf“.

 如此进行,便可以在文本串中找到(或找不到)模式串了.

至此,KMP算法的总体内容已经讲完,核心部分主要是前缀表,下面是代码实现部分.

构造next数组(代码实现)

主要分为以下三步进行:

1.初始化

对于初始化,我们要找前后缀,习惯上,我们定义两个指针i和j.

其中j指向前缀起始位置,i指向后缀起始位置.

然后需要对next数组进行初始化赋值:如下:

	int j = 0;//前缀起始位置
	next[0] = j;//next[i]表示i(包括i)之前最长相等的前后缀长度(其实就是j).

2.处理前后缀不相同的情况

j初始化为0,i应从1开始才有意义,然后进行s[i]和s[j]的比较.所以遍历模式串s循环下标i应从1开始.

	for (int i = 1; i < s.size(); i++)

如果s[i]和s[j]不相同,即前后缀不相同,也需要向前回退.

怎么回退呢?

next[j]就是记录着j(包括j)之前的字串的相同前后缀长度.

那么既然s[i]和s[j]不相同,那么就要找j前一个元素在next数组里的值(next[j-1]).

		while (j > 0 && s[i] != s[j])
		{
			j = next[j-1];//向前回退
		}

3.处理前后缀相同的情况

如果s[i]和s[j]相等,那么就同时向后移动i和j,说明找到了相同的前后缀,同时还要将j(前缀的长度)赋值给next[i],因为next[i]要记录相同的前后缀长度.

代码如下:

		if (s[i] == s[j])
		{
			j++;
		}
		next[i] = j;

所以构建next数组的总代码如下:

void getNext(int* next, const string& s)
{
	int j = 0;//前缀起始位置
	next[0] = j;//next[i]表示i(包括i)之前最长相等的前后缀长度(其实就是j).
	for (int i = 1; i < s.size(); i++)
	{
		while (j > 0 && s[i] != s[j])
		{
			j = next[i];//回退
		}
		if (s[i] == s[j])
		{
			j++;//遇到相同的前后缀,j向前移动,同时i也向前移动(循环++)
		}
		next[i] = j;
	}
}

构造出了next数组,那么我们就可以利用next数组来匹配了.

利用next数组来匹配

在文本串s中,查找是否出现过模式串t.

第一步我们当然要构建好next数组,利用上面所写的

第二步,开始遍历文本串和模式串

不相同,模式串则需要从next数组中寻找下一个位置.

相同,则i和j同时向后移动和

什么时候就算找到模式串t了呢?

可以想到当遍历模式串的j指针等于模式串的长度时,说明已经查找完毕,找到了.

此时返回对应的下标即可.

kmp总代码

void getNext(vector<int>& next, const string& s)
{
	int j = 0;//前缀起始位置
	next[0] = j;//next[i]表示i(包括i)之前最长相等的前后缀长度(其实就是j).
	for (int i = 1; i < s.size(); i++)
	{
		while (j > 0 && s[i] != s[j])
		{
			j = next[j-1];
		}
		if (s[i] == s[j])
		{
			j++;
		}
		next[i] = j;
	}
}
int strStr(string s, string t)
{
	if (t.size() == 0)
	{
		return 0;
	}

	vector<int> next(t.size());
	getNext(next, t);
	int j = 0;
	for (int i = 0; i < s.size(); i++)
	{
		while (j > 0 && s[i] != s[j])//字符串不匹配,利用next数组回退
		{
			j = next[j - 1];
		}
		if (s[i] == t[j])//相等的话,i和j同时++
		{
			j++;
		}
		if (j == t.size())//如果j等于了t的大小,说明找到了,返回文本串中对应模式串的开始字符的下标
		{
			return (i - t.size() + 1);
		}
	}
	return -1;
}
int main()
{
	string s = "leetcode";
	string t = "leeto";
	cout << strStr(s, t) << endl;;
}

好了,kmp的文章到这里就结束了,理解kmp确实是一个晦涩难懂的过程,不过在我们理解了它之后,一切还是很简单的。kmp的核心就是next数组,理解它的作用,kmp基本也就理解了差不多了.

总之,如果你看完本章之后依然有不懂或难以理解的地方,欢迎评论区留言或私信哦.

猜你喜欢

转载自blog.csdn.net/weixin_47257473/article/details/129847970
今日推荐