算法笔记之KMP算法

KMP算法简介:

主要针对字符串的匹配问题,给出字符串a和b,判断b是否为a的子串。时间复杂度为O(m+n) 由Knuth、Morris 和 Pratt 三位科学家共同发现。

next数组简介:

一个字符串s(下标从0开始)则以i号位作为结尾的子串即s[0…i]。对该子串来说,长度为k+1的前缀和后缀分别为s[0…k]和s[i-k…i]。定义next数组,next[i]表示使子串s[0…i]的前缀s[0…k]等于后缀s[i-k…i]的最大的k,若找不到相等的前后缀,则令next[i] = -1。即next[i]是所求最长相等前后缀最后一位的下标。

next数组的求解举栗🌰:

在这里插入图片描述
next数组求解过程:
①i=0:子串s[0…i]为"a",由于找不到相等的前后缀,因此令next[0] = -1。

②i=1:子串s[0…i]为"ab",由于找不到相等的前后缀,因此令next[1] = -1。

③i=2: 子串s[0… i]为"aba",能使前后缀相等的最大的k=0,此时后缀i[i-k…i]为"a",前缀s[0…k]也为"a";而当k=1时,后缀[i-k…i]为"ba",前缀s[0… k]为"ab",不相等,因此next[2] = 0。

④i=3: 子串s[0… i]为"abab",能使前后缀相等的最大的k=1,此时后缀s[i-k…i]为"ab",前缀s[0… k]也为"ab";而当k = 2时后缀s[i- k… i]为"bab",前缀s[0… k]为"aba",不相等,因此next[3] = 1。

⑤i=4:子串s[0… ij]为"ababa",能使前后缀相等的最大的k=2,此时后缀s[i-k…i]为"aba",前缀s[0… k]也为"aba";而当k= 3时后缀s[i-k… i]为"baba",前缀s[0… k]为"abab",不相等,因此next[4] = 2。

⑥i=5:子串s[0… i]为"ababaa",能使前后缀相等的最大的k=0,此时后缀s[i-k…i]为"a",前缀s[0…k]也为"a";而当k = 1时后缀s[i-k…i]为"aa",前缀s[0…k]为"ab",不相等,因此next[5] = 0。

⑦i=6: 子串s[0… ij]为"ababaab",能使前后缀相等的最大的k=1,此时后缀s[i-k…i]为"ab",前缀s[0…k]也为"ab";而当k = 2时后缀s[i-k… i]为"aab",前缀s[0… k]为"aba",不相等,因此next[6] = 1。

求解next数组的一般思路:

①初始化next数组,令j = next[0] = -1。
②i在1~len-1遍历,对每一个i执行③④过程以求解next数组。
③不断令j = next[j],直到j回退为-1,或s[i] = s[j+1]成立。
④若s[i] = s[j+1],则next[i] = j+1,否则next[i] = j。

求解next数组的代码实现:

void getnext(char s[],int len){
	int j = -1;
	next[0] = -1;  //初始化j = next[0] = -1
	for(int i = 1;i < len; i++){
		while(j != -1 && s[i] != s[j+1]){
			j = next[j];  //不断令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
	}
}

KMP算法举栗🌰:

以a=“abababaabc”、b="ababaab"为例,令i指向字符串a的当前想比较的位置,令j指向字符串b中当前已被匹配的最后位,只要满足a[i] = b[j+ 1]成立,则说明b[j + 1]被成功匹配,此时让i、j 加1继续进行比较,直到 j 达到m - 1时说明b是a的子串(m为字符串b的长度)。i指向a[4]、 j指向b[3], 表明b[0… 3]已经全部成功匹配,此时发现a[i] = b[j + 1]成立,表明b[4]成功匹配,则继续令i、j加1。
在这里插入图片描述
接着继续进行配,如下图所示。此时i指向a[5]、j指向b[4],表明b[0…4]已经全部匹配成功。接着继续判断a[i] = b[j + 1]是否成立:若成立,则b[0… 5]被成功匹配,令i、 j加1以继续匹配下一位,然而ButHowever!!!此处a[5] != b[4+ 1],匹配失败。啊这啊这啊这…,难道就此放弃之前b[0… 4]的成功匹配成果、让j回退到-1开始重新匹配???NO!!!
那应该咋办呢?为了不让j直接回退到-1,需要寻求回退到一个离当前的j最近的j’,使a[i] = b[j’ + 1]成立,并且b[0… j’]是与a的相对应位置处于匹配状态,即b[0… j‘]是b[0… j]的后缀。 容易令人想到这就是之前求next数组时碰到的类似问题,b[0… j’]就是b[0… j]的最长相等前后缀。 只需要不断令j= next[j],直到j回退到-1或a[i] = b[j + 1]成立,然后继续匹配即可。next数组的含义就是当j+ 1位失配时,j应该回退到的位置。 当a[5]与b[5]匹配失败时,令j = next[4] = 2,然后惊喜地发现a[i] = b[j + 1]成立,因此继续匹配,直到j = 6也匹配成功,即b是a的子串!!!

在这里插入图片描述

KMP算法的一般思路:

①初始化j = -1,表明字符串b当前已被匹配的最后位。
②让i遍历字符串a,对每一个i执行③④过程以匹配a[i]和b[j+1]。
③不断令j = next[j],直到j回退为-1,或a[i] = b[j+1]成立。
④若a[i] = b[j+1],则 j++,若 j达到 m-1,则说明b是a的子串,返回true。

KMP算法的代码实现(判断b是否为a的子串):

bool KMP(char a[],char b[]){
	int n = strlen(a);
	int m = strlen(b);  //字符串长度
	getnext(b,m);   //求解字符串b的next数组
	int j = -1;  //初始化j为-1,表示当前没有任意一位被匹配
	for(int i = 0;i < n; i++){
		while(j != -1 && a[i] != b[j+1]){
			j = next[j];  //不断回退,直到j回到-1或a[i]=b[j+1]
		}
		if(a[i] == b[j+1]){
			j++;  //a[i]与b[j+1]匹配成功,则j++
		}
		if(j == m-1){  //字符串b是a的子串
			return true;
		}
	}
	return false;
}

通过比较代码发现:KMP算法的代码实现与求解next数组相似的很,于是乎发现next数组的求解过程即是字符串b的自我匹配过程!!!
在这里插入图片描述
接着可以进一步考虑如何统计字符串b在字符串a中出现的次数。例如对字符串a ="abababab"来说,字符串b = "abab"出现了三次,而"ababa"出现了两次。当 j = m- 1时表示字符串b的一次成功完全匹配,此时可以使记录成功匹配次数的变量加1,而之后应该从字符串b的哪个位置开始进行下一次匹配是个问题。下图所示是一次匹配成功的时刻,由于字符串b在字符串a中的多次出现可能是重叠出现的,因此不能直接让i加1继续比较,而应让j回退。此时又是next[j]的高光时刻!!!因为此时next[j]代表整个字符串b的最长相等前后缀,从此位置开始可以让j最大,也就是让已经成功匹配的部分最长,这样就能保证既不漏解,又使下一次的匹配省去无意义的比较。

KMP算法的代码实现(统计字符串b在a中出现的次数):

int KMP(char a[],char b[]){
	int n = strlen(a);
	int m = strlen(b);  //字符串长度
	getnext(b,m);  //计算b的next数组
	int count = 0;  //成功匹配次数
	int j = -1;  //初始化
	for(int i = 0;i < n; i++){
		while(j != -1 && a[i] != b[j+1]){
			j = next[j];  //不断回退,直到j回到-1或a[i]=b[j+1]
		}
		if(a[i] == b[j+1]){
			j++;  //a[i]与b[j+1]匹配成功,则j++
		}
		if(j == m-1){  //字符串b是a的子串
			count++;
			j = next[j];  //使j回退到next[j]继续匹配
		}
	}
	return count;//返回成功匹配次数
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值