408数据结构-KMP算法及其优化 自学知识点整理

前置知识:串的基本概念

关于子串的几个概念:

  • 前缀:指除最后一个字符以外,字符串的所有头部子串。
  • 后缀:指除第一个字符外,字符串的所有尾部子串。
  • 部分匹配值:指字符串的前缀和后缀的最长相等前后缀长度。

例如,对"ababa"这个字符串,其前缀为{a,ab,aba,abab},后缀为{a,ba,aba,baba},相交等于{a,aba},最长的相等的前后缀长度为3
同理,"a"的最长相等前后缀长度为0"ab"0"aba"1"abab"2
故字符串"ababa"的部分匹配值为00123


KMP算法

在朴素模式匹配中,指针的大量回溯及重复比较是其低效率的根源。
若在已匹配相等的前缀序列中,有某个(前缀的)后缀正好是模式串的前缀,则可将模式串向后滑动到与这些相等字符对齐的位置,主串指针i无需回溯,直接从该位置开始继续比较。这就是KMP算法的核心思想。
稍微以我个人的理解讲一下,简而言之,就是在主串和模式串的匹配过程中,若匹配途中出现某一位匹配失败——此时,前面匹配成功的部分中,主串部分出一个后缀,模式串部分出一个前缀,若主串的这个后缀等于模式串的这个前缀,那么,此时的i无需回溯,只需让模式串指针j移动到模式串的这个前缀的下一位继续与主串进行匹配即可。
代码实现如下:

#define MaxLen 259//预定义最大串长,致敬CNCS传奇指挥259
typedef struct {//静态数组存储
	char ch[MaxLen];//字符数组储存串
	int length;//串长
}SString;
int nex[MaxLen] = { 0 };//next数组

int Index_KMP(SString S, SString T) {//KMP算法
	int i = 1, j = 1;
	for (; i <= S.length && j <= T.length; ++i, ++j) {
		if (j == 0 || S.ch[i] == T.ch[j])continue;
		//若j为0或当前位匹配成功,则继续匹配
		else j = nex[j];//否则向右移动模式串
	}
	if (j > T.length)return i - T.length;//匹配成功
	else return 0;//匹配失败返回0
}//最坏时间复杂度O(n+m)

对模式串的这种移动方式,需要预处理一个next[]数组,用于存放模式串的“移动位数”。其实本质上就是对模式串自己和自己进行一次前后缀的“优化过”的朴素匹配(匹配过程中不断地调用了next数组,若模式串长度为m,则时间复杂度为O(m))。

void Get_Next(SString S) {//获取next数组
	nex[1] = 0, nex[2] = 1;
	for (int i = 1, j = 0; i < S.length;) {
		//i记录next数组下标,j记录模式串前缀下标
		if (j == 0 || S.ch[i] == S.ch[j])nex[++i] = ++j;
		//若第i位和第j位匹配,则与主串匹配时到模式串的第i位不匹配的话,模式串直接移动到第j位
		//若能一直匹配下去,则next值的计算实际上等价于next[j+1]=next[j]+1
		else j = nex[j];//否则令j=next[j],若无next值则会让j回到0
	}
/*	//王道书上写法如下
	int i = 1, j = 0;
	nex[1] = 0;
	while (i < S.length) {
		if (j == 0 || S.ch[i] == S.ch[j]) {
			++i, ++j;
			nex[i] = j;//若pi=pj,则next[j+1]=next[j]+1
		}
		else j = nex[j];//否则令j=next[j],循环继续
	}
*/	return;
}

next数组的进一步优化

在模式串中出现大量重复字符的时候,对next数组可以进一步优化处理。
例如,对串"aaaab",若第4位匹配失败的时候,按照之前的算法,next[4]是等于3的,这意味着j的值要回溯到3,而next[3]的值又是2,以此类推,最后一定会回到j=0的时候,并且之前全都匹配失败,因为模式串的前四个字符都相等。
为此,可以对next数组进一步优化。观察可知,当第4位匹配失败时,主串中的当前子串其第4位一定不为a,而模式串的前四位都为a,因此可以直接让j的值回溯到0
next数组再次递归处理,得到nextval数组。此时匹配算法不变,但是把next数组换成nextval数组。

int nextval[MaxLen];
void Get_Val(SString S) {//从预处理的next数组得到nextval数组
	for (int i = 1; i <= S.length; ++i) {//按位循环处理模式串
		if (S.ch[i] == S.ch[nex[i]])nextval[i] = nex[nex[i]];
		//若第i位与第next[i]位字符相同,则nextval[i]直接等于next[i]的next值
		else nextval[i] = nex[i];//否则nextval[i]的值与next[i]的值一样
	}
	return;
}

如果模式串的长度为m,则这种方法得到nextval数组的时间复杂度为 O ( m 2 ) O(m^2) O(m2)。实际上,可以在计算next时对算法略加改进,直接求出nextval

void Get_NextVal(SString S) {//直接求nextval数组
	nextval[1] = 0;//第1位的nextval值一定为0
	for (int i = 1, j = 0; i < S.length;) {//循环处理模式串,i为nextval数组下标
		if (j == 0 || S.ch[i] == S.ch[j]) {
			//若j为0,或者第i位和第j位匹配成功
			++i, ++j;//继续后移一位i和j
			if (S.ch[i] != S.ch[j])nextval[i] = j;
			//若后一位不匹配,则说明匹配到第i位时失配可直接跳到第j位
			else nextval[i] = nextval[j];
			//若后一位匹配,则匹配到第i位失配时跳到的位置和第j位失配跳到的位置相同
		}
		else j = nextval[j];//匹配失败,j跳到nextval[j]的位置
	}
	return;
}

完整代码可以看我的Github:传送门


在408考研初试中,对KMP算法的考察不多,只要求掌握next数组和nextval数组的手算即可,但不失为一个难点。本章知识点需结合习题,进行充分理解,对初学者来说,更是需要反复阅读,手动推演来巩固学习成果。
以上。

  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值