将KMP算法的思路转化成代码思路

前言

在学习KMP算法的过程中,当了解了什么是最长公共前后缀,了解了什么是next数组之后,我们自然想要将KMP算法具体实现出来,那么如何将KMP算法的代码写出来呢,或者说如何去理解KMP算法的代码呢?

一、next数组

首先我们要知道什么是模式串的next数组:

通俗来讲,next数组是一个长度为模式串长度的整型数组在next[i]存放的数值大小即为对应的模式串s从s[0]至s[i-1]这一子串的最长公共前后缀,举个例子:模式串s为:a c t a c b
(前缀用粗体表示,后缀用斜体表示)

下标子串最长公共前后缀长度
0a0
1ac0
2act0
3a ct a1
4ac t ac2
5actacb0

故,该例子的next数组为:{0,0,0,1,2,0}

那么,next数组该怎么用呢?

我们给出文本串 k:a c t a c t a c b

此时来进行匹配:

下标012345678
文本串actactacb
模式串actacb
next000120
模式串下标012345

在匹配到最后一个字符的时候,失配了,这个时候我们要进行模式串的向后滑动,那么滑动到哪里呢?

如果不计较算法的高效,我们只需要向后滑动一位,再重新一位一位比较匹配即可

而我们在得到了模式串next数组的值以后,假如在模式串的第i位失配,则我们先看next[i-1],并将其值所对应的下标“滑动”到失配的位置,话不多说,上图

下标012345678
文本串actactacb
模式串actacb
next000120
模式串下标012345

如图,在i=5时失配,next[4]所对应的值为2,则将模式串下标为2的位置“滑动”到当前的失配位,也就是i=5的位置,如图,模式串其他的元素也会随着一起“滑动”,随后直接从该位置继续匹配。

这个时候,匹配成功,继续匹配,直到模式串的最后一个元素也匹配成功,得出结果,如图

下标012345678
文本串actactacb
模式串actacb
next000120
模式串下标012345

此时,匹配结束,这就是通过next数组进行匹配的流程

原因

next数组为什么会这么神奇呢?为什么按照这样来“滑动”,可以把之前的一部分模式串直接跳过呢?相比这是大多数人刚学KMP算法的时候所思考的。我们再回到刚刚失配时的操作:

下标i012345678
文本串actactacb
模式串actacb
next000120
模式串下标j012345

如图,不难看出,在失配的时候,k[i]与s[j]自然是不相等的,这个时候我们就需要将模式串索引为next[j-1]的元素移动到i这个位置。

我们可以得到以下等式:
k [ i − m ] = s [ j − m ] = , m = 1 , 2 , … n e x t [ j − 1 ] k[i-m] =s[j-m]=,m=1,2,…next[j-1] k[im]=s[jm]=,m=1,2,next[j1]
s [ m ] = s [ l e n − 2 × n e x t [ j − 1 ] + m ] , m = 1 , 2 , … n e x t [ j − 1 ] , l e n = s . s i z e ( ) s[m] = s[len-2\times next[j-1]+m],m = 1,2,…next[j-1],len = s.size() s[m]=s[len2×next[j1]+m],m=1,2,next[j1],len=s.size()

看起来有点头晕,我们就拿上面那个来举例子

next[j-1]是2
则第一个等式的意思是,在失配位之前有2个字符,模式串与文本串是匹配的(即图中绿色的两组子串)
下标i012345678
文本串actactacb
模式串actacb
next000120
模式串下标j012345
而第二个等式的意思是,在失配位之前,存在长度为2的相同的前后缀,也就是最大公共前后缀(即图中绿色的两组子串)
下标i012345678
文本串actactacb
模式串actacb
next000120
模式串下标j012345

由这两个等式就可以得出一个结论:在出现失配之前,会有长度为next[j-1]的最大公共前后缀已经在这次比较时匹配过,在模式串“滑动”的时候,我们就可以跳过这些已经匹配过的前缀,而前缀的长度正好是next[j-1],所以我们只需要将s[next[j-1]这个位置移动到失配位,这个时候,模式串的前缀正好和刚刚匹配过的文本串的后缀重合,不需要再次匹配。

求next数组

在明白了原理以后,我们就需要根据模板串来获得其next数组,现在让我们一起试试吧。

由于这个很难从零讲清楚,我先放上代码,随后通过解析代码来分析

//获取模式串的next数组
		void getNext(int *next, string needle){
			int j = 0;
			next[0] = 0;
			for(int i = 1;i<needle.size();i++){
				while(j>0 && needle[i]!=needle[j]){
					j = next[j-1];
				}
				if(needle[i]==needle[j]) j++;
				next[i] = j;
			}
		}

首先,我们传入一个空的next数组,并将模式串needle传入。

int j = 0; next[0] = 0;

这里j有两个含义,一个是当前比较的游标,一个是最大公共前后缀的长度。
由于模式串的第一个子串是不会存在前后缀的,最大公共前后缀长度为0,故设next[0]为0

for循环

从模式串的第二位遍历到模式串最后一位

next[i]的确定分两步,我们可以想象一下,此时j为最大公共前缀的长度,而needle[j-1]代表了最大公共前缀的最后一位。

如果needle[i]==needle[j]:则needle[j]也是是当前子串的最大公共前缀的一部分,则j应该自增1,在自增的同时,将j下一次比较的游标向后挪一位。

在j自增以后,由于needle[i]所在的位置已经是当前子串的最后一位,则不会再有更长的公共前后缀,所以,将j的值赋给next[i]即可。则可以把代码补全如下:
//获取模式串的next数组
		void getNext(int *next, string needle){
			int j = 0;
			next[0] = 0;
			//处理前后缀相等的情况
			if(needle[i]==needle[j]) j++;
				next[i] = j;
			}
		}
如果needle[i]!=needle[j]:在前后缀不相等时,说明最大公共前后缀长度不可以再增加,我们需要往回退,而往回退的时候,我们希望在之前得到的最大公共前后缀中,获得其最大公共前后缀,并将游标j定位到上一次出现该前缀的位置。

举个例子:在求模式串:a c a a c c 的next数组的过程中
已经得到了这些元素:

模式串acaacc
next001
i,j的位置ji
index012345
此时,needle[I]!=neelde[j],j=1,我们看到 a 这个字符不能加入到最大公共前后缀,也就是说 ac不会是这个子串的最大公共前后缀,原最大公共前后缀为a,那么,在原最大公共前后缀这个子串中,也存在着最大公共前后缀,这个例子中最大公共前后缀的最大公共前后缀长度为next[0]=0,不会出现a是这个当前最大公共前后缀的前缀的下一位的可能性,所以回到模式串开头,这个时候needle[i] == needle[j],回到上文中的情况。
模式串acaacc
next0011
i,j的位置ji
index012345
理论上,如果能不断找到最大公共前后缀的最大公共前后缀,在发生不匹配后应该继续往前寻找,直到匹配成功或者回到模式串开头,所以应该有两个条件进行回溯,并且在回溯结束后还要判断needle[i]和needle[j]是否相等,如果到最后都不相等,说明最大公共前后缀为0,此时正好是j的值,所以补全代码得
//获取模式串的next数组
		void getNext(int *next, string needle){
			int j = 0;
			next[0] = 0;
			for(int i = 1;i<needle.size();i++){
				while(j>0 && needle[i]!=needle[j]){
					j = next[j-1];
				}
				if(needle[i]==needle[j]) j++;
				next[i] = j;
			}
		}

实现KMP算法

在上文中,我们已经获得了next数组,现在我们需要通过next数组来匹配文本串和模式串了,想到这里就会很头疼,为什么,好像又要写一段逻辑来实现这个功能了,其实不然,请看:

在求next数组中,我们做了以下工作:

  1. 求出模式串的所有前缀子串的最大公共前后缀的长度
  2. 将求出的长度放到前一个下标所在的位置

那么在实现字符串匹配的时候,我们能不能用相同的思路呢?答案是肯定的。

我们把问题再回到:“字符串匹配”上来,字符串匹配需要文本串和模式串遍历到最后,直到模式串的最后一位与文本串完全匹配,就得到了最后的结果。有没有觉得和求next数组的过程很像。

如果我们把模式串和文本串拼接起来,组成一个新串

例如:文本串为:actactacfaa 模式串为:actacf
则求出next数组后,实现匹配的思路本质上可以看作:

求出串:(actacb)(actactacbaa)
的next数组中第一个为模式串actacf长度6的元素的下标并作出处理,(括号仅仅用于分开模式串和文本串,没有其他含义)

此时,下标为0,直接从文本串部分开始,我们求出其next数组如下:

下标012345012345678910
文本串actacbactactacbaa
next000120123453456!!

可以看出,在i=8的时候,出现了next[8] = 6,也就是说,i = 8时正好是这个子串的末尾,所以可以得到模式串在文本串中出现的下标为i-needle.size()+1=3,也正是最终答案。

所以代码与上部分大同小异,就不详细解释

int kmp(string haystack,string needle){
			//空串直接返回0
			if(needle.size()==0) return 0;
			//文本串长度小于模式串直接返回无法找到
			if(needle.size()>haystack.size()) return -1;
			int j = 0;
			int next[needle.size()];
			getNext(next,needle);
			for(int i=0;i<haystack.size();i++){
				while(j>0 && haystack[i] != needle[j]){
					j = next[j-1];
				}
				if(haystack[i] == needle[j]) j++;
				//如果找到了长度为要求的最大公共前后缀,直接退出
				if(j == needle.size()) return (i-j+1);
			}
			return -1;
		}

最后

这个算法理解还是需要一些时间,思路在理解了最大公共前后缀后并不难想出来,但是将其转化为实际的代码却是能让人想破脑袋,佩服能写出这个算法的大佬!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值