KMP算法详细解析(c语言篇)

1、什么是KMP算法

K        MP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n) [1] 。来自-------百度百科。

2、KMP算法有什么用

 快速模式匹配算法,简称 KMP 算法,是在 BF 算法基础上改进得到的算法。

要是没有了解BF算法的话可以去看看我之前写的文章:(49条消息) 拳打脚踢解决BF算法QAQ_banbanbanni的博客-CSDN博客

BF算法就是暴力算法,挨个遍历主串来寻找子串,这种方法其实效率并不高,而且也比较浪费时间,

KMP的算法接近人为进行模式匹配的过程.就比如我们现在有一个主串"ABCABCE",我们需要在这个主串中寻找一个子串"ABCE"。

BF的方法我们不难发现我们匹配了四次才匹配到了子串的位置

 我们不难发现为了寻找这个子串我们i连续遍历了4次,其间还不算j对于子串的遍历每次一主串遍历失败,我们子串的 j就得从零开始 ,i也得从 i - j + 1开始从头再来 ,显得很麻烦。效率很低

 但是我们用KMP就可以简单很多,KMP的遍历就在于他的i 会一直往前走,不会后退,而且他的 j也不会因为一次失败匹配就倒退回到 j = 0的位置。

 这样就便利了很多,那么问题来了,我们改如何判断j应该下一次应该跳到什么位置呢?

答案是next数组,但是next数组是什么呢?我们如何构建他呢?

接下来我们来探究探究他的奥秘所在。

 三、什么是NEXT数组?

其实next数组就是一个前缀表,什么是前缀表呢

 前缀表就是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

就用刚刚的嘞个KMP算法的例子来看当我们发现f != b的时候按理来说我们应该我们应该从头再来慢慢的匹配这个东西,但是他并没有,我们的前缀表就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。

那么前缀表是如何记录的呢?接下来我用一张图来解释这个前缀表的一个记录过程

 我们不难发现,实际上就是记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

因此这个字符串的前缀表为 0 1 0 0 2 0。

而我们的next数组实际上是把数组整体往右边移动一位,然后数组第一位为 -1,数组最后一个元素舍去,其实这并不涉及到KMP的原理,而是具体实现,next数组即可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。

 四、如何去实现next数组

构建一个next数组的过程其实也就是构建一个前缀表的过程

1.构建数组

首先我们得构建一个数组并且初始化数组

int* next =(int*)malloc(sizeof(int)*sublen);//因为和子串长度一样

2.初始化数组

     next[0] = -1;
	 next[1] = 0;
	 int i = 2;//当前的i下标
	 int k = 0;//代表前一项的k

 k从第一位开始遍历前缀,i 往后开始遍历后缀,因此初始化 i = 2 ,k = 0;

这里我们提前给i+1,因此i是从2开始遍历,实际上我们需要的是 i - 1,也就是原本的i + 1 - 1 = i

3.当前缀 == 后缀时

这里我是用了一个反证法来求出当前缀 == 后缀时

假如这是一个我们已经求好的next数组,我们就用他来反证一下我们需要的结果

首先我们假设next[i] = k 

我们在这个数组中不难发现 p[0]到p[k -1]的长度等于 p[x] = p[i - 1] ,然后我们来求这个x

不难发现  k - 1 - 0 == i - 1 - x 就可以求出x = i - k ,所以这个等式可以变成

那如果 p[ k ] == p[ i ]呢,如图

 对比上面next[i] = k时候的等式不难发现

 这边也就解释了为什么我们要提前给i加1,然后算的时候给i-1,实则就是next[(i - 1) + 1] = k + 1

 因此当前缀 == 后缀时代码为

 if ((k == -1) || (sub[k] == sub[i - 1]))//因为数组整体往右加一
		 {
			 //当sub[k] == sub[i]
			 //next[i+1] == k + 1 
			 next[i] = k + 1;
			 i++;
			 k++;
		 }

 4.当前缀 !=后缀的时候

也就是说我们的p[i] != p[k]

当我们p[i] != p[k]时我们的k会一直回退,直到找到p[k]  == p[i]然后才继续进行第三步

k = next[k];

5.最终代码的实现

void Getnext(int* next, const char* sub)
{
	int len = strlen(sub);
	 next[0] = -1;
	 next[1] = 0;
	 int i = 2;//从next[2]开始遍历
	 int k = 0;
	 while (i < len)
	 {
		 //i一直往前走,k可以变换
		 if ((k == -1) || (sub[k] == sub[i - 1]))//因为数组整体往右加一
		 {
			 //当sub[k] == sub[i]
			 //next[i+1] == k + 1 
			 next[i] = k + 1;
			 i++;
			 k++;
		 }
		 else
		 {
			 k = next[k];
		 }
	 }
}

五、如何用代码实现KMP算法?

void Getnext(int* next, const char* sub)
{
	int len = strlen(sub);
	 next[0] = -1;
	 next[1] = 0;
	 int i = 2;//从next[2]开始遍历
	 int k = 0;
	 while (i < len)
	 {
		 //i一直往前走,k可以变换
		 if ((k == -1) || (sub[k] == sub[i - 1]))//因为数组整体往右加一
		 {
			 //当sub[k] == sub[i]
			 //next[i+1] == k + 1 
			 next[i] = k + 1;
			 i++;
			 k++;
		 }
		 else
		 {
			 k = next[k];
		 }
	 }

}
int kmp(const char* str,const char* sub, int pos)
{
	//str为主串,sub为子串
	assert(str && sub);
	int i = pos;
	int j = 0;
	int lenstr = strlen(str);
	int lensub = strlen(sub);
	int* next = (int*)malloc(sizeof(int) * lenstr);
	assert(next);
	Getnext(next,sub);
	while (i < lenstr && j < lensub)
	{
		if ((j == -1) || (sub[j] == str[i]))
		{
			//j == -1时next[0] = -1
			j++;
			i++;
		}
		else
		{
			j = next[j];
		}
	}
	free(next);
	if (j >= lensub)
	{
		return i - j;
	}
	return -1;

}

六、时间复杂度分析

其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。

暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大的提高的搜索的效率。

猜你喜欢

转载自blog.csdn.net/weixin_64448174/article/details/123657939