KMP算法详解,涵盖你对kmp算法所有的不解之处

1.KMP算法引入

在实现该代码之前,我们要先知道KMP算法是什么,有什么用,和一般的暴力搜索方法比较起来有何种优势。
首先KMP算法是什么:
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)
看完上面的概述之后,不难发现KMP算法是用于查找一个字符串里面是否存在我们想要的子串,例如在abcdefg中查找有无abc这个子串
那么他和一般的暴力搜索的优势在哪?举个例子

char arr1[] = "aabaabaac";
char arr2[] = "aabaac";

在这里插入图片描述
暴力搜索:一开始一 一匹配,当匹配到arr2的c时,和arr1的c并不匹配,p1指针重置成arr1数组的第二个元素,p2置回arr2数组的起始位置,由此类推,直到p1走到arr1的末尾或者p2走到arr2的末尾。我们可以发现,这样方法并没有好好利用我们先前匹配成功的部分,每一次查找之前没有任何关系,以至于做了很多的无用功。
那么KMP算法便解决了问题,他最大的优势就在于充分的利用了已匹配的部分,减少了无用匹配次数。

2.KMP算法实现要求

(设被查找的字符串为文本串arr1,想要的字符串的称为模式串arr2)
KMP算法最大的核心就是通过前缀表next记录模式串中首元素到每个元素的子串的最长相等前后缀。什么意思呢?举个例子
首元素到每个元素的子串,即:

char arr[] = "aabaac";//a aa aab aaba aabaa aabaac

前缀:包括首字符但不包括尾字符的子串。

char arr[] = "aabaac";//前缀:a aa aab aaba aabaa

后缀:包括尾字符但不包括首字符的子串。

char arr[] = "aabaac";//前缀: a ab aba abaa abaac

所以例子中的next数组就为

在这里插入图片描述
至于数组元素的内容从何而来接下来我将一一介绍:

char arr[] = "aabaac";//a aa aab aaba aabaa aabaac

next[0]=0,即a,由于只有一个元素,所以前后缀均是a,所以最长相等前后缀为0
next[1]=1,即aa,前缀a,后缀a,所以最长相等前后缀为1
next[2]=0,即aab,无相等前后缀,所以为0
next[3]=1,即aaba
易错点来了!!!
next[4]=2,即aabaa,可能有同学会问这里不应该是三吗,前缀aab,后缀也aab啊,这里需要强调一点,前后缀不是回文子串,前后缀都应该从一个方向读:也就是aab是不等于baa的,如果子串是aabaab,这时最长相同前后缀便是3。

next[5]=0,即aabaac
介绍完前缀表后,我们应该怎么使用呢?
具体用法:

char arr1[] = "aabaabaac";
char arr2[] = "aabaac";

当我们p2指到c时,发现文本串和模式串并不相同,这是不再是把p2(见下图)指针置回开头,而是找到next数组中c前一个(即next[p2-1])的相同前后缀的长度max_lenth,然后从arr2[max_lenth]继续和arr1那个位置匹配。例如:
【找到next数组中c的前一个(即next[4])的相同前后缀的长度2,然后从arr2[2]继续和arr1那个位置匹配,因为是前一个所以这里需要小心数组越界问题,这里源代码注释会说】
在这里插入图片描述
在这里插入图片描述
为什么可以这么做呢?很明显利用的前后缀相同的前提。 因为后缀和前缀相同,所以我们节省了匹配前缀的那一段,不就减少了多余操作了吗。
知道了这些,接下来最重要的就是处理next数组的每元素,那怎么样对next数组的元素进行赋值呢? 对于这样一个模式串

char arr[] = "aabaac";//没错还是我

想要求出每一段子串的最长的相同前后缀,我们需要用到两个指标分别指向前缀的尾元素和后缀的尾元素。如图:
在这里插入图片描述
如果两个指针指向的元素相同,令pprefix++,然后next[psuffix]=pprefix,随后再psuffix++,如果不相同,令pprefix = next[pprefix - 1]后再比较两个指针指向的元素是否相同,若仍不相同则重复上面操作,直到pprefix=0或者两元素相同时停止,令pprefix++,然后next[psuffix]=pprefix,随后再psuffix++,直到psuffix到达模式串末尾,若以pprefix=0,依然要执行next[psuffix]=pprefix。
看完这里大家有疑问,接下来我为大家统一解释一下

Q1:为什么next[psuffix]=pprefix?我们知道next是用于存储模式串首元素到每个元素的那个子串最长的相同前后缀的长度,而这个长度就是前缀长或者后缀长。
Q2:pprefix = next[pprefix - 1]这一步什么意思?其实求最长相等前后缀也可以等同于子串匹配,只不过这个是在同一个字符串上匹配的,举个例子

char arr[] = "aabaac";//没错又又又是这个模式串

在这里插入图片描述
这样next数组的赋值操作就完成啦!

3.源代码

{
	int sz1 = strlen(arr1);
	int sz2 = strlen(arr2);
	int* next = (int*)calloc(sz2, sizeof(int));
        int pprefix = 0;//pprefix不仅代表前缀长,也可代表最长相等前后缀
        //next数组元素赋值
	for (int psuffix = 1; psuffix < sz2; psuffix++)//next[0]默认为0  
	{
		//不相等的情况
		while (pprefix>0&&arr2[pprefix]!=arr2[psuffix])
		{
			pprefix = next[pprefix - 1];
		}
		//相等的情况
		if (arr2[pprefix] == arr2[psuffix])
		{
			pprefix++;
		}
		next[psuffix] = pprefix;//更新next
		//printf("%d ", next[psuffix]);//可用于测试你next数组是否正确
	}
	int pmatch = 0;//模式串指针
	int parr1 = 0;//文本串指针
	while (parr1 < sz1&&pmatch<sz2)
	{
		if (arr1[parr1] == arr2[pmatch])
		{
			parr1++;
			pmatch++;
		}
		else
		{
		//首先能走到下面这个if判断条件,说明一定不相等
                //所以如果模式串回到首元素位置,说明文本串指针需要++了
                //同时也避免了数组越界情况
                        if (pmatch == 0)
			{
				parr1++;
			}
			else
			pmatch = next[pmatch - 1];
		}
	}
	free(next);
	next = NULL;
	if (pmatch == sz2)//模式串结束了说明匹配成功
		return 1;
	else
		return 0;
}

作者:璃月偶像刻晴
链接:https://juejin.cn/post/7082384910320664583/
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

若有错误,感谢指出!!!

我们这里说的KMP不是拿来放电影的(虽然我很喜欢这个软件),而是一种算法KMP算法是拿来处理字符串匹配的。换句话说,给你两个字符串,你需要回答,B串是否是A串的子串(A串是否包含B串)。比如,字符串A="I'm matrix67",字符串B="matrix",我们就说B是A的子串。你可以委婉地问你的MM:“假如你要向你喜欢的人表白的话,我的名字是你的告白语中的子串吗?” 解决这类问题,通常我们的方法是枚举从A串的什么位置起开始与B匹配,然后验证是否匹配。假如A串长度为n,B串长度为m,那么这种方法的复杂度是O (mn)的。虽然很多时候复杂度达不到mn(验证时只看头一两个字母就发现不匹配了),但我们有许多“最坏情况”,比如,A= "aaaaaaaaaaaaaaaaaaaaaaaaaab",B="aaaaaaaab"。我们将介绍的是一种最坏情况下O(n)的算法(这里假设 m<=n),即传说中的KMP算法。 之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。这时,或许你突然明白了AVL 树为什么叫AVL,或者Bellman-Ford为什么中间是一杠不是一个点。有时一个东西有七八个人研究过,那怎么命名呢?通常这个东西干脆就不用人名字命名了,免得发生争议,比如“3x+1问题”。扯远了。 个人认为KMP是最没有必要讲的东西,因为这个东西网上能找到很多资料。但网上的讲法基本上都涉及到“移动(shift)”、“Next函数”等概念,这非常容易产生误解(至少一年半前我看这些资料学习KMP时就没搞清楚)。在这里,我换一种方法来解释KMP算法
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值