【手把手带你学会KMP算法】

本文详细介绍了字符串匹配中的KMP算法,首先回顾了暴力求解的BF算法,然后重点讲解了KMP算法的核心思想和next数组的求解过程,包括如何避免不必要的回溯以提高效率。最后,讨论了KMP算法的优化,引入了nextval数组以进一步提升匹配速度。
摘要由CSDN通过智能技术生成

相信大家在遇到字符串匹配问题时,无论是听老师上课讲还是在网上查询资料时几乎都会用到KMP算法,本篇博客借鉴于大博哥对于KMP算法的分析以及自身对于KMP算法的看法,相信认真看完了后会对你有一些帮助。

目录

 1 BF 算法

2 KMP 算法

3 KMP算法的优化



 1 BF 算法

了解KMP算法之前,我们先来回忆一下BF算法(暴力求解),基本思想就是主串中元素与子串中元素一一比较,匹配失败就让子串返回到0下标,主串回溯到开始匹配的下一位。

假设主串的长度位M  子串的长度为N  BF算法的时间复杂度为 O(M*N)

动图详解(此图借鉴于其他博主):

 具体代码:

int BF(char* mainStr, char* subStr,int pos)
{
	assert(mainStr && subStr);
	if (!*subStr)
	{
		return 0;
	}
	if (!*mainStr)
	{
		return -1;
	}

	int lenMain = strlen(mainStr);
	int lenSub = strlen(subStr);

	int i = pos; //遍历主串
	int j = 0; //遍历子串

	while (i < lenMain && j < lenSub)
	{
		if (mainStr[i] == subStr[j])
		{
			i++;
			j++;
		}
		else
		{
			i = i - j + 1;
			j = 0;
		}
	}

	if (j == lenSub)
	{
		return i-j;
	}
	else
	{
		return -1;
	}
}

2 KMP 算法

KMP算法的诞生:KMP算法是三位大牛:Knuth、Morris和Pratt同时发现的,于是取了他们名字的首字母然后组合起来,就成了该算法的命名。(来自百度)

 kmp算法的主要思想是:在主串中查找子串时,利用主串和子串中已经匹配的部分,跳过一些无用的比较,使主串的标记指针不会回溯,子串的标记指针移动。其实主要是子串中如果有部分内容和主串中一致,我们需要研究这些已知的匹配内容,这相当于我们需要研究子串,发现子串中存在的规律。KMP算法的时间复杂度为 O(M+N)

 听起来是不是有点懵,没关系,我们来举个栗子:

主串:ababcabcdabcde

子串:abcd

由上述主串与子串数据可知目前在索引为2匹配失败,就算让主串回退到1也是没有必要的,因为主串中1位置字符b与子串0位置的a也不一样,所以我们的目的是:主串是不回退的,让子串回退到特定的位置

那么子串应该回退到什么位置呢? 再来举个栗子:

主串:abcababcabc

子串:abcabc

这里主串与子串在索引为5的位置不匹配,由于主串不回退,回退的是子串,通过肉眼观察我们发现子串回退到索引为2是比较合理的(我们要尽量在主串中找到和子串匹配的一部分串),由于主串中索引为5的字符a!=子串中索引为2的字符c,所以还要继续回退,直到回退到与字符a相等为止。那如果字符数量多了用肉眼观察肯定是不切合实际的,所以我们引出了next数组,用来保存子串中某个位置匹配失败后回退的位置

KMP的精髓就是next数组,那么应该怎样来求next数组呢?假定next[j]=k;

求k的规则:

  1. 找到匹配成功部分的两个相等的真子串(不包含本身)一个以下标0开始,另一个以j-1下标结束
  2. 总有next[0]=-1,next[1]=0。

有了上面的规则我们就可以来求next数组了:

栗子1:“ababcabcdabcde"

它对应的next数组为:[-1,0,0,1,2,0,1,2,0,0,1,2,0,0]

栗子2:”abcabcabcabcdabcde"

它对应的next数组为:[-1,0,0,0,1,2,3,4,5,6,7,8,9,0,1,2,3,0]

通过上面的栗子我们可以得出总结:找的时候可以找重复的,但是必须是以0下标开始,找的前一位下标结束;而且数组中元素增大只能一个一个的加,减小可以跳跃减小。

到这里我相信大家对求next数组应该没有什么问题了。但现在的问题是我们如何用公式来求next数组,总不能next数组让我们人为来算,这显然是不现实的。

我们假设已知:next[i]=k;

 
                    k         i
给定一个字符串" a b c a b a b c a b c "

             0 1 2 3 4 5 6 7 8 9 10

next数组:    [-1,0,0,0,1,2,1,2,3,4,5]

我们假设此时i==8

则我们可以知道:p[0]……p[k-1] == p[i-k]……p[i-1]

所以由:next[i]=k  <--->   p[0]……p[k-1] == p[i-k]……p[i-1]

如果现在 p[k]==p[i]  将其分别加在上式右边的两边得:

p[0]……p[k] == p[i-k]……p[i]

所以不难推出:next[i+1]=k+1;这也很好得解释了为什么增加只能一个一个的加。

那么问题又来了,如果p[k] != p[i]呢?

假设索引为8的元素由a改为b,那么k就要回退到next[k]的位置,也就是k=0,但还是不满足p[k]==p[i],就继续回退k=-1,此时已经找不到了,就直接将0给予给下一位,也就是:

next[i+1]=0;

 好了,next数组的求解概念已经掌握,现在我们如何用代码来将其表示出来呢?

代码的具体实现:

int KMP(char* mainStr, char* subStr, int pos)
{
	assert(mainStr && subStr);
	if (!*subStr)
	{
		return 0;
	}
	if (!*mainStr)
	{
		return -1;
	}

	int lenMain = strlen(mainStr);
	int lenSub = strlen(subStr);
	//动态申请next数组
	int* next = (int*)malloc(sizeof(int) * lenSub);
	if (NULL == next)
	{
		exit(-1);
	}
	GetNext(next,subStr);

	int i = pos; //遍历主串
	int j = 0; //遍历子串

	while (i < lenMain && j < lenSub)
	{
		if (mainStr[i] == subStr[j] || -1==j)
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}

	if (j == lenSub)
	{
		free(next);
		return i - j;
	}
	else
	{
		free(next);
		return -1;
	}

}

这里面有一个容易出错的点,当j==-1时也不要忘记了处理,当j==-1时说明回到了索引为0的位置,并且还没有找到与其匹配的项,就让j从索引为0开始与i一一比对。

得到next数组的具体代码:

void GetNext(int* next, int* subStr)
{
	assert(next && subStr);
	int lenSub = strlen(subStr);
	next[0] = -1;
	next[1] = 0;
	int i = 2;
	int k = 0;

	while (i < lenSub)
	{
		if ((-1 == k) || subStr[i - 1] == subStr[k])
		{
			next[i] = k + 1;
			i++;
			k++;
		}
		else
		{
			k = next[k];
		}
	}
}

由于这里的next[i]是未知的,而next[i-1]才是已知的,所以上面才是subStr[i - 1] == subStr[k]

next[i] = k + 1; 同理当k==-1时说明到索引为0都还没有匹配的项就把其置为0.


3 KMP算法的优化

假定给一个这样的字符串: " a a a a a a a a a a g "

它的next数组为:               [-1,0,1,2,3,4,5,6,7,8,9]      

 假设在索引为9的位置匹配失败,则j会不断回退直到j==-1,这样效率就比较低了,有什么办法能够一步到位回退呢?

我们不妨优化一下next数组:为了区别,不妨将将优化后的数组取名为nextval

nextval数组的规则如下:

1 如果回退到的位置和当前字符一样,就写回退那个位置的nextval值;

2 如果回退到的位置和当前字符不一样,就写当前原来的nextval值;

 举个栗子:给" a  b  c  a  a  b  b  c  a  b  c  a  a  b  d  a  b "

next:               [-1, 0, 0, 0, 1, 1, 2, 0, 0, 1, 2, 3, 4, 5, 6, 0,1]

nextval:          [-1, 0, 0,-1, 1, 0, 2, 0,-1, 0, 0,-1, 1, 0, 6,-1,0]

具体代码:

void GetNextval(int* nextval, int* subStr)
{
	assert(nextval && subStr);
	int lenSub = strlen(subStr);
	nextval[0] = -1;
	nextval[1] = 0;
	int i = 2;
	int k = 0;

	while (i < lenSub)
	{
		if ((-1 == k) || subStr[i - 1] == subStr[k])
		{
			nextval[i] = k + 1;
			i++;
			k++;
		}
		else
		{
			k = nextval[k];
		}
	}
	i = 2;
	while (i < lenSub)
	{
		if (subStr[i] == subStr[nextval[i]])
		{
			nextval[i] = nextval[nextval[i]];
		}
		i++;
	}
}

好了,KMP算法就到这里了,如果有什么不对的地方欢迎各位佬在评论区指正。

  • 72
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 91
    评论
评论 91
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值