KMP算法教学

之前有讲过KMP的模板,当时自己也是刚学KMP也没有对这个算法有太深的理解,只是浅浅的了解。所以打算回来写一篇,让读者能明白的KMP算法的教学。耐住性子看20分钟你也可以明白KMP算法的精髓。

KMP的算法是针对与解决两个字符串的匹配问题。

首先我们先介绍一下这个算法需要用到的数据结构。

Next【】:next数组又被称之为失配数组,简单来说就是当字符串str1和字符串str2不匹配时。按照暴力求解的方法是字符串str1的开始位置++,然后拿这个位置的字符和字符串str2的0位置去比较。但是引入Next数组这个东西之后,字符串str2不会从头开始比较,而是选择当前比较位置之前的某一个位置进行比较。这样就对比较过程进行了加速。这是Next数组的大致作用,具体作用会在之后解释,先让大家先初步了解这个Next数组的作用。

KMP算法的精髓就是在这个Next数组上。

明白Next数组的大致作用之后。我们再了解两个概念。

1)前缀:abcdefg的前缀分别为a,ab,abc,abcd,abcde,abcdef。

2)后缀:abcdefg的后缀分别为g,fg,efg,defg,cdefg,bcdefg。

我们为什么要明白这两个概念的呢?

因为Next数组的每个位置的储存的结果是从头位置到当前位置的【的最长前缀和最长后缀的相等的字符串的长度】

但是Next数组又有一点小变动,就是前缀不能包含最后一个字符,后缀不能包含第一个字符。为了简化我们把这个要求称之为要求①

不理解上面没关系,我举几个例子就好了。

如字符串 abcdabcd。

第一个字符a,没有前缀和后缀。我们把它Next【0】 = -1

第二个字符b,他只有唯一的前缀和后缀a,我们定义了要求①,所以 我们把Next【1】= 0。

第三个字符c,他有长度为1的前缀字符串a,和长度为1的后缀字符串b,不相等,所以Next【2】= 0

第四个字符d,它有长度为1的前缀字符串a,和长度为1的后缀字符串c,不相等,他又有长度为2的前缀字符串ab,和长度为2的后缀字符串bc,不相等所以Next【3】= 0;

第五个字符a,它有长度为1的前缀字符串a,和长度为1的字符串d,不相等,他又有长度为2的前缀字符串ab,和长度为2的后缀字符串cd,不相等,他还有长度为3的前缀字符串abc,和长度为3的后缀字符串bcd,嗯全都不相等,所以Next【4】 = 0

第六个字符b,他有长度为1的前缀字符串a,和长度为1的字符串a,!!!相等。然后他有长度为2的前缀字符串ab,和后缀字符串da,不相等,然后他还有长度为3前缀字符串的abc,和后缀字符串cda,不相等。他还有长度为4个前缀字符串abcd和后缀字符串bcda,然后不相等。所以最长的相等的前缀和后缀的长度为1,所以Next【5】 = 1;

第七个字符c,

前缀字符串分别为 a,ab,abc,abcd,abcda,

后缀字符串分别为 b,ab,dab,cdab,bcdab,

所有相等的字符串为ab,所以最长的相等的前缀和后缀的长度为2,Next【6】= 2;

第八个字符d

前缀字符串分别为 a,ab,abc,abcd,abcda,abcdab

后缀字符串分别为 c ,bc,abc,dabc,cdabc,bcdabc,

所有相等的字符串为abc,所以最长的相等的前缀和后缀的长度为3,Next【7】= 3

所有Next数组为

a   b  c   d  a  b c d

0   1  2  3  4  5  6 7

-1  0  0  0  0  1  2 3

现在大家应该对Next数组有个初步了解了。至于如何用怎么求,在这里我先不展开讲,等最后大家会了KMP流程的之后,再讲。现在大家先明确一个概念就是,Next的储存的是什么。然后我们怎用它。

所以现在大家就认为有个函数能求出某个字符串的Next数字,我们把这个函数定义为GetNext()。

明白之前的一些概念之后。我们来讲如何用Next数组来实现KMP算法。

现在有字符串str1

a b c d a b c x a b c d a b c d x y z @

还有字符串str2

a b c d a b c d

我们现在问str2是否在str1出现过。

先求出str2的Next【】,再定义i 为 str1匹配到的位置,j为str2匹配到的位置

 

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
-1 0 0 0 0 1 2 3                        
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
a b c d a b c x a b c d a b c d x y z @
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
a b c d a b c d                        

于是我们开始匹配,我们发现在第7个位置失配。这时我们不把str2从头开始匹配,而是看一下next数组,发现next【7】为3.

让 j = Next【 j = 7】= 3,于是我们把str数组向右“推”3位。

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
a b c d a b c x a b c d a b c d x y z

@

 

        0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
            a b c d a b c d                

我们发现我们上面的那步操作的实质是,让str【7】的‘x’和str【3】‘d’去匹配。为什么?

因为我们能很明显的发现str1【4-6】和str2【0-2】是相等的。

这一步操作的实质否定了,字符串str1从1位置能和字符串str2的可能性。此外还后定了2位置,3位置。

但是我们没有否定4位置之后(包括4位置)的可能性。

为什么这些位置不可能和str2的匹配上。现在我给大家证明一下。

假设我们在str1的2位置能和str2匹配上。那么起码2位置到失配位置7要能和str2去匹配上,对吧?否则一定推不出从2位置能匹配上。也就是说从字符串str1的2位置到6位置的所有字符,要和字符串str2的0位置到位置4的字符全部相等。相等长度为4。而我们之前又匹配过一遍,那就是到str1的0-6的所有字符都和str2的0-6相等。也就是说str1的0-6能对应到str2的0-6位置。那么变成了str2的0-4位置和str2的2-6位置的所有字符全都相等,大家发现了没?这是Next【7】位置上的信息。而str2的7位置Next数组记录的是0-6的最长前缀和最长后缀的相等的长度,长度为3。和这个矛盾了。所以同理我们也能否定了2和3这个位置。

证明比较绕口,需要大家多看两遍。这就是KMP能加速的原因,他把不可能匹配上的结果全都抛弃掉了。

然后我们发现字符串的str1的7位置和字符串的str2的4位置还是匹配不上,你们我们继续看一下,Next【j】 = Next【3】= 0,那么我们让 j = 0,实际上这步操作是,否定了4 , 5 ,6这三个位置的可能性,并让str1在i = 7的位置 和str2从0开始匹配。

每当我们遇到失配时,我们就让 j = next【j】,来否定掉所有不可能的情况,继续匹配。我们会发现str1【i = 7】 != str2【j = 0】。而这时候next【0】  = -1,其实就意味着,str2的第一个字符不匹配, 那么我们只需要i++即可。若匹配就i++,j++。

这就是KMP的流程。和正确性的证明。文字比较多,我希望读者如果读不明白,可以多看几遍,因为别的博客不会给你讲证明这么详细。

所以KMP的流程如下

若匹配 则 i ++ ,j ++ 继续进行下个字符匹配。

若不匹配,判断Next【j】是否为-1

若为-1表示,str2的第一个都不匹配,那么i++

若为>0的数,那么就让j = next【j】。去否定不可能的情况。

流程讲完之后,我们在再考虑Next数组怎么求。

对于一个Next数组,我们把Next【0】 = -1,Next【1】 = 0,这些是显而易见的信息。

我们求是从Next【2】来求的。首先我们要着重强掉一个内容,就是Next数组储存的是什么信息。

前缀和后缀的最长相等的长度。那么对于下个Next【i+1】的值而言我们是可以通过Next【i】来求出来的。

怎么求?方法如下。

我们只需要判断 i 位置的字符,是否和str【next【i】】的字符相等即可,若想等就Next【i+1】= Next【i】+ 1;不等就让str【i】和 str【Next【i】】+1的位置比较;相等 就让Next【i+1】 = Next【Next【i】】++;

举个例子大家看一下

a   b  c   d  a  b c d

0   1  2  3  4  5  6 7

-1  0  0  0  0  1  2 

假设我现在求到了Next【7】的。我们根据Next数组的定义可知,Next【6】存的是abcdab的最长前缀和后缀的相同部分。且我们能看出来是长度为2的字符串ab,现在在求Next【7】位置时,前缀基本没有变,而后缀是在Next【6】的基础上都加了一个c字符。

第七个字符c,

前缀字符串分别为 a,ab,abc,abcd,abcda,

后缀字符串分别为 b,ab,dab,cdab,bcdab,

所有相等的字符串为ab,所以最长的相等的前缀和后缀的长度为2,Next【6】= 2;

第八个字符d

前缀字符串分别为 a,ab,abc,abcd,abcda,abcdab

后缀字符串分别为 c ,bc,abc,dabc,cdabc,bcdabc,

所有相等的字符串为abc,所以最长的相等的前缀和后缀的长度为3,Next【7】= 3

这个规律很明显能发现我们,每加一个字符,判断他的最长前后缀只需让这个前一个字符和Next【i】的下一个位置的字符比较即可。若不想等,就把Next【i】的位置再分成两部分比较。直到没法分。然后让Next【i+1】 = 0.否则就让他等于相等的位置的

Next【i+1】 = Next【j】++;

这个做法依旧可以用假设法来证明是正确的。

依旧是这个例子

a   b  c   d  a  b c d

0   1  2  3  4  5  6 7

-1  0  0  0  0  1  2 

我们假设7位置的正确答案是4。那么就证明0-3位置和3-6是相等的。那么我们可以推出来,0-2和3-5是相等。但是Next【6】位置是2.所以矛盾所以不正确。

那么再假设。7位置的答案是2,那么就说0-1和5-6的位置是相等的。但是Next【6】=2的说明,0-1和4-5是相等。说明4-5和5-6是相等的。很明显是不对的。

所以能证明这种做法的正确性。

到此所有的做法都完成了。

剩余是代码的问题了。

#include
using namespace std;
const int maxn = 100000;
int Next[maxn];   // 失配数组 
void GetNext(char s[]){
	int len = strlen(s);
	Next[0] = -1;   //初始化 0 1位置 
	Next[1] = 0;
	int i = 2;   // 记录现在在求那个位置的next 
	int cn = 0;  // 当前的字符的前一个字符的next【】数组的指向位置的下一个位置。 
	while(i < len){
		if(s[i-1] == s[cn])
			Next[i++]= ++cn;  // 若相等。则当前位置++,当前位置的字符的前一个字符
								//的Next【】数组指向位置的下一个的位置++; 
		else if(cn > 0)
			cn = Next[cn];	// 若不想等。判断是否大于0,判断是否在头位置。若不在头位置
							// 就让cn = Next【cn】;  
		else 
			Next[i++] = 0;  // 否则代表 当前位置没有和他相等的前缀。所以i++,并next【i】++; 
	}
} 
int Kmp(char s1[],char s2[]){
	int len1 = strlen(s1);
	int len2 = strlen(s2);
	GetNext(s2);
	int i = 0,j = 0;
	while(i < len1 && j < len2){   
		if(s1[i] == s2[j] ){  // 相等就i++,j++ 
			i ++;
			j ++;
		}
		else {
			if(Next[j] == -1)  // 若为头位置 就表示 当前i位置没有相等。 
				i ++;
			else     
				j = Next[j];  
		}
	}
	if(j >= len2) return i - j;
	else return -1;
}

int main(){
	char s1[maxn];
	char s2[maxn];
	cin >> s1 >> s2;
	int ans = Kmp(s1,s2);
	cout << ans << endl;
}

 

你可能感兴趣的:(字符串)