KMP算法疑惑点探讨

前言

KMP算法是一种字符串匹配算法,即在一个字符串中查找是否存在另一个字符串,此算法在面试中属于热门题,对于他的实现方式以及代码相信大家都了解过,但对于一些细节、及为什么要这样做可能没有深究。
本文旨在解答KMP算法中一些被网上教程、博文一笔带过但困扰大家的问题。

1.KMP算法与基础概念简介

1.1前后缀

前缀:除最后一个字符以外,字符串的所有头部子串
后缀:除第一个字符外,字符串的所有尾部子串;
比如
ababa,前缀就是a,ab,aba,abab;后缀就是a,ba,aba,baba

1.2最长相等前后缀长度

即前后缀相等的最大子串的长度
上面例子里,前后缀相等的最大子串就是aba,长度为3

1.3模式串和主串

主串就是已知字符串;
模式串就是要查找的字符串。
比如:
在abcdefg里查找cde,abcdefg就是主串,cde就是模式串

1.4普通匹配

即两层循环,逐个字符对比
发现不匹配的字符,则主串、模式串指针重置,模式串移动到主串的下一个位置重新开始匹配
比较简单,不再赘述

1.5KMP算法

核心思想:
主串不回退,模式串自己回退。
且没必要每次都回退到开头。
比如下面的情况
下一次匹配的,应该是下图的位置
这里其实的下标就是abcabc的最长相等前后缀长度
至于为什么,后面会讲解,大家先知道这个是最长相等前后缀长度即可。
所以有了这个结论,我们要计算出所有位置的最长相等前后缀长度,用于匹配失败的时候模式串回退
这也就是KMP的next数组,存储所有位置前面的最长相等前后缀长度

2.问题解答

2.1为什么移动的长度与相等前后缀长度有关?

假设主串和模式串在图示位置匹配失败,一个是A一个是B
将模式串向右移动一部分后(或者是模式串回退,都一样,便于理解此处采用右移),重新匹配
请添加图片描述
图中竖线中间的位置是相等的,因为上面的模式串到A才匹配失败,说明前面的肯定都匹配了
第二个模式串匹配成功,当然也是相等的
那么我们只看两个模式串,是不是发现A前面的两位与开头的两位是一样的?这就是相等前后缀
所以如果匹配成功,那么一定是这个子串在匹配失败位置的前面的子串的相等前后缀

2.2为什么移动的位置是最大相等前后缀?

见下图,假设模式串匹配到A时失败了,A前子串的最大相等前后缀长度是3(红色部分和黄色部分),还有相等前后缀X,长度是1
请添加图片描述
主串下的第一个模式串是以非最大相等前后缀为标准移动的情况,第二个是以最大相等前后缀为标准移动。
首先可以看到,如果第二个匹配成功了,那第一个情况就把第二个给错过了,因为模式串已经把第一位移过来了,即模式串已经回退到第一位了,所以此时如果不匹配,主串会继续下个为止,再也不会出现第二个模式串的情况了。
其次,如果第二个情况匹配失败,那么如果继续找当前失败位置子串的,但还是至少有有相等前后缀X,此时继续往下进行,仍可以到第一种的情况
因此,用非最大相等前后缀,可能会遗漏匹配的结果。
用最大相等前后缀,成功了正好,没成功也会走到与用非最大相等前后缀一样的结果。
所以移动的位置是最大相等前后缀

3.求next数组

定义next[i]为第i位前子串的最大相等前后缀长度
定义子串为S
基于动态规划的思想
假设现在要求next[i],next[0]->next[i-1]都已知了
分下面几种情况来探讨

3.1 第i-1位与next[i-1]位相等

如果S[i-1] == S[next[i-1]],则next[i] = next[i-1] + 1;
为什么可以这样做呢?请看下图
请添加图片描述
我们现在要求next[A],假设next[B] = 3,即图中红色部分是相等的。
如果B和C相等,那么A前4位与S前4位就是一致的,即A有相等前后缀长度4。
那么如何确定这里得到的就是最大相等前后缀呢?

3.2会不会有更大的相等前后缀?

在上一个的基础上,假设A的最大相等前后缀是5,即下图的情况(蓝色部分)
请添加图片描述

可以看到,如果蓝色部分相等
next[B],应该是4,这与之前定义的next[B]=3相违背
所以可以证明,不会有更大的了。
如果第i-1位与next[i-1]位相等,那么next[i] = next[i-1] + 1;

3.2 第i-1位与next[i-1]位不相等

如果S[i-1] != S[next[i-1]],那么就与S[next[i-1]]再进行比较,回到步骤一
可能有点难理解,请看下图
请添加图片描述
假设next[B] = 4,next[C] = 2
计算next[A],先对第i-1位与next[i-1]位进行比较,即比较B和C,发现不相等之后
按照上面说的,再对比next[C]和B,也就是D,如果相等了,那么回到步骤3.1,next[A] = next[D] + 1;

3.3 为什么可以这样做呢?

我们再回到之前的图
next[B] = 4,说明下方红色部分是相等的
请添加图片描述
next[C] = 2,说明C左侧红色与蓝色部分是相等的
请添加图片描述
那么既然第一个图片里红色部分相等,第二张图片里C左侧红色与蓝色相等
我们就可以判断出,子串里下面四个区域都是相等的请添加图片描述
也就是说,蓝色区域与棕色区域是相等的
那如果B和D相等,是不是就能拿到A的相等前后缀了?
即next[A] = 3

3.4 怎么保证是最大的?

前面我们已经讨论过了,next[A]最多比next[B]大1,也就是说A前子串的最大前缀,最多就是到C
但此时B!=C,所以A的最大前缀一定在C往左
所以假设next[A] = 4
即如下图,两个蓝色区域是相等的
请添加图片描述
又因为next[B] = 4
所以下方的红色区域是相等的
请添加图片描述
综合我们可以得出:
1 2 3 4 = 5 6 7 8
1 2 3 4 = 6 7 8 9
(上述是各位一一对应的)
所以1 2 3 = 6 7 8 = 2 3 4
所以得出next[C] = 3,与预设的next[C] = 2不符。
所以next[A] = 3即为最大
有点混乱,本质是通过假设next[A]比用next[C]计算出来的还要大,反过来计算next[C],以推翻这种情况。
所以我们保证了此种方式计算出来的是最大的。

4.代码展示

4.1求next数组

std::vector<int> getNext(const string &pattern) { 
	std::vector<int> next(pattern.size());    
	next[0] = 0; /* 第0位前没有子串,自然是0 */
	int cur = 1; /* 代表要求的当前索引 */    
	int pre = 0; /* 代表上一位的next值 */     
	while (cur < pattern.size())     
	{         
		if (cur == 1) /* 处理特殊情况,当前位置是1时,前面子串长度是1,没有前后缀,自然是0 */
		/* 在外面手动处理也行,比如next[1] = 0,然后cur = 2 */         
		{             
			next[cur] = 0;             
			cur++;             
			continue;         
		}         
		if (pattern[cur - 1] == pattern[pre]) /* 相等,那么等于上一个+1 */         
		{             
			next[cur++] = ++pre;         
		}         
	else /* 不相等 */         
		{             
			if (pre == 0)  /* 特殊情况处理,如果已经回退到0了,没有回退空间了,且0这个位置已经比较过了,说明没有前后缀了,next置为0,去看下一位*/                             
			{
				next[cur] = 0;                 
				cur++;                 
				continue;             
			}             
			pre = next[pre]; /* 回退到上一个的next */
			}     
	}     
	return next; 
}

4.2KMP

int kmp(const string &basic, const string &pattern) {         
	std::vector<int> next = getNext(pattern);    
	int i = 0;         
	int j = 0;         
	while (i < basic.size() && j < pattern.size())
	{             
		if (basic[i] == pattern[j]) /* 匹配成功,往下走 */             
		{                 
			i++;                 
			j++;                 
			continue;
		}             
		if (j == 0) /* 特殊情况处理,如果到了第0位都不一样,那就说明不可能一样了,主串往下走 */
		{                 
			i++;                 
			continue;             
		}             
		j = next[j]; /* 模式串回退 */         
		}
		if (j == pattern.size()) /* 到头了说明匹配成功*/
		{
			return i-pattern.size();
		}         
		return -1;    
}

5.总结

本文主要讲解了KMP算法以及next数组求解中大家可能会疑惑的点,即讲解了一些为什么可以这样做的依据。
代码可能与网上的不一样,只是用我觉得方便大家理解、接收的方式书写的,还有优化的空间。
鉴于本文只是讲解依据性的问题,不再对代码的优化进行深究。

  • 25
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值