【知识点2】KMP算法⭐⭐⭐⭐⭐

0. 引子:字符串匹配问题

本节主要讨论字符串的匹配问题

0.1 定义

如果给出两个字符串textpattern,需要判断字符串pattern是否是字符串text的子串。一般把字符串text称为文本串,而把字符串pattern称为模式串

0.2 暴力解决方案

在这里插入图片描述在这里插入图片描述

1. KMP算法

下面介绍KMP算法,时间复杂度为 O ( n + m ) O(n+m) O(n+m)。它是由Knuth、Morris、Pratt这三位科学家共同发现,这也是其名字的由来。

1.1 next数组

1.1.1 定义

在正式进入KMP算法之前,先来学习一个重要的数组——next数组

假设有一个字符串s(下标从0开始),那么它以i号位作为结尾的子串就是s[0...i]。对该子串来说,长度为k+1(注意是k+1)的前缀和后缀分别是s[0...k]s[i-k...i]
现在定义一个int型数组next(请先不要在意名字),其中next[i]表示使子串s[0...i]的前缀s[0...k]等于s[i-k...i]的最大的k注意前缀跟后缀可以部分重叠,但不能是s[0...i]本身);如果找不到相等的前后缀,那么就令next[i]=-1。显然,next[i]就是所求最长相等前后缀中前缀最后一位的下标。

需要充分理解 next[i]所表示的含义

1.1.2 示例

以对字符串s="ababaab"作为举例,next数组的计算过程如下所示,读者可以结合图12-1进行理解。图中对每个next[i]的计算都给出了两种阅读方式,其中上框直接用下画线画出了子串s[0...i]的最长相等前后缀,而下框将子串s[0…i]写在两行,让第一行提供后缀,第二行提供前缀,然后将相同的最长前后缀框起来。建议先看懂上框中的过程,再去看懂下框中的过程,相信两种方式会给出不一样的体验。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
再强调一遍,next[i]就是子串s[0…i]的最长相等前后缀的前缀最后一位的下标。
相信到了这里,读者已经完全知道什么是next数组了。

1.1.3 求解next数组

那么,怎么求解next数组呢?当然暴力的做法是可行的,但是不高效。下面用“递推”(即从已知的中推出未知的)的方式来高效求解next数组,即假设已经求出了next[0]~next[i-1],现在要用它们来推出next[i]

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

next[0]等于-1的原因是 前缀和后缀不能是s[0…i]本身。

代码:

/*
涉及的变量
s[i]:表示输入的数组 
next[i]:表示s[0...i]中的最长相等前后缀中前缀最后一位的下标 
i:表示当前计算的s[0...i]
j:表示上一次计算的next[i]的值
*/ 

//getNext求解长度为len的字符串s的next数组
void getNext(char s[],int len){
	j = -1;
	next[0] = -1;	//初始化j = next[0] = -1 
	for(int i=1;i<len;i++){	//求解next[1] ~ next[len-1] 
		while(j !=-1 && s[i] != s[j+1]){
			j = next[j];
		}	//直到j回退到-1,或是s[i]==s[j+1] 
		if(s[i] == s[j+1]){		//如果s[i] == s[j+1] 
			j++;	// 则next[i] = j+1 ,先令j指向这个位置 
		}	
		next[i] = j;	//令next[i] = j 
	}
} 

1.2 KMP算法

在前面学习了next数组之后,我们来开始KMP算法的真正学习。读者会发现,有了上面的基础,KMP算法就是依样画葫芦。此处给定一个文本串text和一个模式串pattern,然后判断模式串pattern是否是文本串text的子串。

text=“abababaabc”pattern=“ababaab”为例。如图12-6所示,令i指向text当前欲比较位,令j指向pattern中当前已被匹配的最后位,这样只要text[i] == pattern[j+1]成立,就说明pattern[j+1]也被成功匹配,此时让ij加1以继续比较,直到j达到m-1说明patterntext的子串(m是模式串pattern的长度)。在这个例子中,i指向text[4]、j指向pattern[3],表明pattern[0...3],表明pattern[0...3]已经全部匹配成功了,此时发现text[i] == pattern[j+1]成立,这说明pattern[4]成功匹配,于是令ij加1。
在这里插入图片描述

接着继续匹配,如图12-7所示。此时i指向text[5]j指向pattern[4],表明pattern[0...4]已经全部匹配成功。于是判断text[i] == pattern[j+1]是否成立:如果成立,那么就有pattern[0...5]被成功匹配,可以令ij加1以继续匹配下一位,但是十分不幸的是,此处text[5] != pattern[4+1],匹配失败。似乎很让人懊恼,难道就此放弃之前pattern[0...4]的成功匹配结果、让j回退到到-1开始重新匹配吗?当然不会,只有暴力的做法才会那么做!
在这里插入图片描述
那应该怎么做呢?
为了不让j直接回退到-1,应寻求回退到一个离当前的j最近的j’,使得text[i] == pattern[j'+1]能够成立,并且pattern[0...j']仍然与text的相应位置处于匹配状态,即pattern[0...j']pattern[0...j]的后缀。这很容易令人想到之前求next数组时碰到的类似问题,答案是pattern[0...j']就是pattern[0...j]的最长相等前后缀。也就是说,只需要不断令j = next[j],直到j回退到-1或是text[i]==pattern[j+1]立,然后继续匹配即可。

从这个角度讲,next数组的含义就是当j+1位失配时,j应该回退到的位置。

对图12-6的例子来说,当text[5]pattern[5]匹配失败时,令j = next[4] = 2,然后惊喜地发现text[i] == pattern[j+1]能够成立,因此就让它继续匹配,直到j==6也匹配成功,就意味着pattern是text的子串。

因此可以总结出KMP算法的一般思路:
①初始化j = -1,表示pattern当前已被匹配的最后位。
②让i遍历文本串text,对每个i,执行③④来试图匹配text[i]pattern[j+1]
③不断令j = next[j],直到j回退为-1,或是text[i] == pattern[j + 1]
④如果text[i] == pattern[j+1],则令j++。如果j达到m-1,说明pattern是text的子串,返回true。

KMP算法的代码如下:

/*
KMP算法,判断pattern是否是text的子串 
涉及到的变量 
text:文本串
pattern:模式串
 
*/ 
bool KMP(char text[],char pattern[]){
	int n = strlen(text),m = strlen(pattern);	//字符串长度
	getNext(pattern,m);		//计算pattern的next数组
	int j = -1;		//初始化j = -1,表示当前还没有任意一位被匹配
	for(int i = 0;i < n;i++){	//试图匹配text[i] 
		while(j != -1 && text[i] != pattern[j+1] ){
			j = next[j];		//不断回退,直到j回退到-1或者text[i]==pattern[j+1] 
		}
		if(text[i] == pattern[j+1]){
			j++;		//text[i]与pattern[j+1]匹配成功,令j加1 
		}
		if(j == m-1){	//pattern完全匹配成功,说明pattern是text的子串 
			return true; 
		}
	} 
} 

读者会发现这段代码和求解next数组的代码惊人地相似。事实上稍加思考就会发现,求解next数组的过程其实就是模式串pattern进行自我匹配的过程。
接着思考如何统计文本串text中模式串pattern出现的次数?

例如对文本串text = "abababab"来说,模式串pattern=“abab”出现了三次,而模式串pattern=“ababa”出现了两次。

在这里插入图片描述
统计模式串pattern出现次数的KMP算法代码如下所示:

//KMP算法,统计pattern在text中出现的次数
int KMP(char text[],char pattern[]){
	int n = strlen(text), m = strlen(pattern);	
	getNext(pattern,m);
	int ans = 0,j = -1;
	for(int i=0;i < n;i++){
		while(j!=-1&&text[i] != pattern[j+1]){
			j = next[j];
		}
		if(text[i] == pattern[j+1]){
			j++;
		}
		if( j == m-1){
			ans++;
			j = next[j];
		}
	}
	return ans;
} 

2. 关于时间复杂度

可能会有人问,既然for循环中每个i都有一个while循环,这样j回退的次数可能不可估计,为什么KMP算法的复杂度是 O ( n + m ) O(n+m) O(n+m) 呢?

首先,整个for循环中i是不断加1的,所以在整个过程中i的变化次数是O(n)级别,这个应该没有问题。接下来只考虑j的变化,我们注意到j只会在一行中增加,并且每次只加1,这样在整个过程中j最多只会增加n次;而其他地方的j都是不断减小的,由于j最小不会小于-1,因此在整个过程中j最多只能减少n次(否则j就会小于-1了),也就是说while循环对整个过程来说j最多只会执行n次,因此j在整个过程中的变化次数是O(n)级别的(可以认为均摊到每次for循环中就是O(1))。由于i和j在整个过程中的变化次数都是O(n),因此for循环部分的整体时间复杂度就是O(n)。考虑到计算next数组需要O(m)的时间复杂度(用同样的分析方法可以得到),因此KMP算法总共需要O(n+m)的时间复杂度。
在这里插入图片描述

3. KMP算法的再优化

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述

//getNextVal求解长度为len的字符串s的nextval数组
void getNextVal(char s[],int len){
	int j = -1;
	nextval[0] = -1;	//初始化j = nextval[0] = -1
	for(int i=1;i<len;i++){
		while(j != -1 && s[i] != s[j+1]){
			j = nextval[j];
		}
		if(s[i] == s[j+1]){
			j++;
		}
		//与getNext函数相比只有下面不同
		if(j == -1 || s[i + 1] != s[j + 1]){
			nextval[i] = j;
		}else{
			nextval[i] = nextval[j];
		} 
	} 	
} 

在这里插入图片描述在这里插入图片描述在这里插入图片描述

4. 从有限状态自动机的角度看待KMP算法

在这里插入图片描述在这里插入图片描述

5. 题型训练

KMP算法的考察方式很灵活,除了考察最基本的子串匹配问题,还会在next数组上进行其他形式的考察。

一、直接进行模式串与文本串的匹配 / 统计模式串在文本串中出现的次数(裸题)

  1. 字符串匹配掌握了吗?

二、使用next数组求解循环节

  1. POJ 1961 Period
  2. ⭐⭐⭐⭐⭐【KMP & 最小循环节】Power Strings
    循环节模板如下
    int len = strlen(s);
    //得到最小循环节
    int mod = len-1 - nex[len-1];
    if(len%mod==0){
    	printf("%d\n",len/mod);
    }else{
    	printf("1\n");
    }
    
    需要注意的是周期为1的情况在ifelse中都会出现,不仅仅是else中会出现,比如第1道题POJ 1961 Period里面就是这种情况。

三、前缀后缀问题

  1. ⭐⭐⭐⭐⭐Seek the Name, Seek the Fame
  2. Simpsons’ Hidden Talents

6. 参考文档

  1. 算法笔记
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值