C语言KMP算法(便于理解版本)

一.前言

笔者经过学习并对KMP算法思考理解,想在本文与各位大佬萌新进行讨论,也算是笔者对这一算法学习的总结,供大家参考,并可以对更简单的理解给予我私信,共同进步。

本文从三个方面对KMP算法进行讨论

1.从BF算法引入KMP

2.next数组的含义

3.求解next数组

二.正文

2.1从BF算法引入KMP

BF算法就是我们说的暴力匹配,我们知道主串中包含了很多个与我们要找的模式串长度相同的子串,所以暴力匹配算法相当于把这些子串都和模式串compare了一次,但是我们在真实的比较过程中不是把所有子串找出来,而是移动指针比较,如图:

我们现在就可以移动P1和P2指针进行比较,当P1和P2相等时,继续向后比较,如果不等那么我们需要把模式串向后移动一位,继续前面的操作如图:

当指针指向P1和P2所示,表示不满足,此时我们认为这个A开头的子串不可能匹配了,就把模式串移动到B开头的子串进行比较,为了方便我后面图把主串令为S1,模式串为S2:

所以只需将模式串按照上面步骤依次比较,直到找到我们想要匹配的子串为止,当满足以下两个条件时结束:

1.当P2等于斜杠0了,证明找到了(有些教材用的是字符串的长度判定)

2.当 P1等于斜杠0,证明找不到

但是细心的朋友比较上面两个图发现,当我重新从B进行比较的时候,P1回到了B的位置,P2回到了A的位置,我们把这个称作指针回溯,每一次都要重新把主串回溯到上一次比较的下一个位置,把模式串回溯到开头。代码供参考

char* my_strstr(const char* arr1,const char* arr2) 
{                                             //加上const是因为我们只需要查找不改变指针内容
	if (*arr2 == '\0')
	{
		return arr1;
	}
	while (*arr1)
	{
		char* s1 = arr1;                        //s1和s2是为了记录回溯位置
		char* s2 = arr2;
		while ((*s1!='\0')&&(*s1 == *s2)&&(*s2!='\0'))
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
		{
			return arr1;
		}
		arr1++;
	}
	return NULL;
}
int main()
{
	char arr1[] = "aaababcdef";
	char arr2[] = "abc";
	char*p1=my_strstr(arr1, arr2);
	if (p1 == NULL)
		printf("没有找到");
	else
		printf("%s\n",p1);
	return 0;
}

算法分析:因为我们每个字符子串都要比较一次,所以如果主串长度为m,模式串长度为n的话,我们最坏的打算是要比较(m-n)*n次,时间复杂度为O(m*n)。

如何优化呢: 我们来看一个例子

 上图匹配到s1[4]时会退出本次匹配回溯指针从s1[1]也就是B处开始第二次子串匹配,但是我们会发现移动模式串的时候到s1[1],s1[2]都不行,因为我们模式串开头为A,就是说我们必须要遇到开头是A的才有可能匹配成功,意思是说我们比较过的字符中,存在相同的前缀和后缀时,才会去考虑这时候的子串是否可能匹配,意味着我不需要回溯主串的指针,直接拿主串匹配失败的时刻的字符与模式串中的某个元素进行比较,具体是哪个元素,我们把这个元素的位置用一个数组存起来,这个数组就是next[j]。

2.2next的含义

刚刚初步认识了next[j]是什么,是在某个元素匹配失败的时候,主串指针不动,存储模式串指针回溯的位置的数组,而在匹配成功的时候,模式串和主串已经匹配的字符是相同的,即我们不需要看主串长什么样子,只需要知道模式串就OK:

如ABCDAB,指针在A时候A没有前缀后缀,指针在B时候,B前缀为A,没有后缀,指针在C时候,前缀为A,后缀为B,指针在D时候,前缀为A,AB,后缀为C,BC,笔者用枚举的方式解释前后缀,为了避免匹配过程中遗漏掉可能成功的子串,要找到匹配失败前的最长相同前后缀。

next[j]的双重含义:

1.next[j]表示匹配失败时该字符前面最长的相同前后缀,相同前后缀用K计数;

2.next[j]表示匹配失败后,模式串应该回溯的指针位置。

拿模式串ABCDABE,在E匹配失败时,前面有最大长度为2的相同前后缀AB,相当于我们要把AB向后移动:

 

 直接用s2[2]与s1[6]比较,上述的next含义是规律,读者不必纠结为什么会有这个规律,规律是拿来发现的,不是拿来创造的。

此时因为不必回溯s1的指针,算法变得很简单了,时间复杂度为O(m+n)。

2.3next数组的求解

先看这一个问题如果我们已知了上一个字符的next[j]值,怎么求下一个呢,有两种情况:

1.char S[ ]="ABCDABE",对于字符串S来说,比如我们知道S[5]时next的值为1,那S[6]的next的值可以直接比较S[1]与S[5]就ok,如果相等,那么S[6]next的值就是前一个next值加1,即为2

2.当S[1]与S[5]不相等时,看个图

 指针在P2位置,红色部分是P2之前的相同前后缀,我们发现红色部分完美的关于ED对称,此时有K个相同的前后缀,K为6,即next[j]为6,如果想要知道P2之后的D的next,我们需要拿P2和P1比较,发现P2不等于P1,证明了找不到长度更长的相同前后缀,只能找更短的,但要使得要和E前面的部分要相同

 当P1指针跳到P3时候,绿色部分刚好与E之前的部分相同,这就是next数组的含义了,它表示当前字母最长的相同前后缀,并且指向对应的位置,我们再比较P2和P3,P2和P3相同,相当于P2后面D的next值就为,P1处的next值加1就行了,此时next为3,如果P3和P2还不相等,那么我们就只能继续找P3的next处的值继续比较,原理很简单,因为相同的前后缀一定对称,比如P2前的红色框是关于ED的完美翻折。

重复以上步骤,它就是一个递归的思想代码如下

int my_KMP(char* des, char* sre,int* next)
{
	int slen = strlen(des);
	int plen = strlen(sre);
	int i = 0;
	int j = 0;
	while (i < slen && j < plen)
	{
		if (j == -1 || des[i] == sre[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}
	if (j == plen)
	{
		return i - j;
	}
	else
		return -1;
}
void get_next(int next[], char*sre )
{
	int len = strlen(sre);
	int j = 0;
	int k = -1;                  //因为j==0没有前缀,所以为了方便,K初始值为-1
	next[0] = -1;               //同样的把next数组的初始值也令为-1
	while (j < len - 1)
	{
		if (k == -1 || sre[k] == sre[j])  //sre[k]表示前缀,sre[j]表示后缀
		{
			++k;
			++j;
			next[j] = k;
		}
		else
		{
			k = next[k];           //next数组迭代,K表示的是最长的前后缀长度
		}
	}                             //k=next[k]的目的是匹配不成功后找到对称的位置
}
int main()
{
	char arr1[] = "abcdefaaaabbbaaccc";
	char arr2[] = "aabbba";
	int next[100] = { 0 };
	get_next(next, arr2);
	int p1=my_KMP(arr1, arr2,next);
	printf("%d\n", p1);
	return 0;
}

里面注意的细节我都标注在代码里面,KMP就是next难找,笔者初学时,脑袋炸了。。。。。

另外有一篇博主写的很详细,我在这个基础上加入了自己的理解,因为写太多了好多时候就懒得去看文字,本文参考链接如下

从头到尾彻底理解KMP(2014年8月22日版)_v_JULY_v的博客-CSDN博客_从头到尾彻底理解kmp

 这是个让人膜拜的大佬。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值