数据结构与算法学习笔记——KMP算法

本文详细介绍了KMP算法的原理和实现,通过避免暴力匹配中的回溯提高效率。首先,阐述了模式匹配的基本概念和传统BF算法的不足,然后讲解了KMP算法的核心思想——部分匹配表的构建及其作用,最后展示了如何通过nextval[]进一步优化KMP算法,减少不必要的比较。通过实例分析,帮助读者理解KMP算法的逻辑并不复杂。
摘要由CSDN通过智能技术生成

数据结构与算法学习笔记(C语言)

KMP算法以及改进

定义:子串的定位操作通常叫串的模式匹配,Index(S, T, pos),其中T称为模式串,模式匹配操作是各种串处理操作过程中最重要的操作之一

比如在人体的DNA序列中查找是否存在某病毒序列以判断这个人是不是感染的病毒,就要用的模式匹配操作。

之前我们已经写了下面这个模式匹配算法,它调用了求子串的函数以及串比较函数。

int Index(String S, String T, int pos)
{
	int m, n, i; 
	String Sub;
	if (pos > 0) {
		n = S[0]; m = T[0]; i = pos;
		/*用到了求串长的操作*/
		while (i <= n - m + 1) {
			SubString(Sub, S, i, m);
			/*用到了求子串操作*/
			if (StrCompare(Sub, T) != 0) ++i;
			/*用到了比较串的操作*/
			else return i;
		}
	}//if
	return 0;
}

现在,我们还用定长顺序存储结构,不借助其他操作,重写一下模式匹配算法。最简单直观的算法是BF算法。
模式匹配

由上图可知,模式匹配的步骤为:
设置计数变量i ,j,其中i记录主串进行比较的字符位置,j记录模式串进行比较的字符的位置
刚开始,i = 1,j = 1;字符不同。然后 ++i = 2,j = 1;不同。然后 ++i = 3,j = 1;不同。然后 ++i = 4,j = 1;相同。然后 ++i,++j,也即 i = 5,j = 2;相同。然后 ++i,++j,也即 i = 6,j = 3;不同。
需要回头比较 i = 5,j = 1;也即 i = i - j + 2,j = 1;这里因为之前相同,i、j都递增了相同的次数, i = i - j 使得递增的次数被消除,i 等于之前和 j = 1 比较的位置减1,加上二等于下一个需要和 j = 1 比较的位置
在这个例子中就是 6 - 3 + 2 = 4 - 1 + 2 = 5,那么就是回头比较 i = 5,j = 1;

相同就递增i,j,不同就回头比较,直到 i = 9,j = 1;相同。i = 10,j = 2;相同。i = 11,j = 3;相同。模式匹配完成!

int Index(String S, String T, int pos) {
/*返回子串T在主串S中第pos个字符之后的位置。若不存在,则返回0。其中T不空*/
	int i, j; //计数变量
	i = pos; j = 1;
	while (i <= S[0] && j <= T[0]) {
		if (S[i] == T[j]) {
			++i;
			++j;
		}
		else {
			i = i - j + 2;
			j = 1;
		}
	}//while
	if (j > T[0]) return i - T[0];
	/*匹配成功,返回S和T匹配的第一个位置*/
	else return 0;
}	

上述算法就是中规中矩的模式匹配算法,过程容易理解,简单直接。如果每次发生不匹配都是在第一个字符,那直到匹配,算法的时间复杂度是

					O(Length(S) + Length(T))

如果是下面这种情况,这个匹配算法效率就很低了

					S:aaaaaaaaaab  T:aaab

因为每次都要比较到模式串的最后一个字符,才发生不匹配,算法的时间复杂度一下变成

					O(Length(S) * Length(T))

我们知道,这个算法的效率低根源在于需要不停地回溯,每次失配时,i 总是回退到 i - j + 2 的位置,而 j 总是回退到 1,下面介绍改进的模式匹配算法。

KMP算法:这种算法由 Knuth、Morris 和 Pratt 同时设计实现,因此简称KMP算法。改进之处为:每当一趟匹配过程中出现字符不相等时,不需要回溯 i 指针,而是利用已经得到的“部分匹配”的结果,让指针 j 退回到 next(j) 的位置上重新进行比较。
改进算法
改进的匹配算法在第一趟 c 和 a 失配后,并没有回溯 i,i = 3,让 j 回退到 1,再进行第二趟比较,当又再 i = 7 失配时,j = 2,第三趟匹配完成!
KMP

记 j 需要回溯的位置为 next[j],那么整个KMP算法需要确定的就是 next[j] 的表达式。
我们先把代码写在下面,然后再去解读

void getnext(String T, int next[])
{
	int i = 1; int j = 0;
	next[1] = 0;
	while (i < T[0]) {
		if (j == 0 || T[i] == T[j]) {
			++i;
			++j;
			next[i] = j;
		}
		else j = next[j];
	}
}

这里 next[1] = 0,在 1 位置失配,就会拿1 位置的字符和主串的下一个字符比较。next[2] = 1,是 2 位置前不会有相等的两端,而且在 2 位置失配的话就会再拿 1 位置的字符去跟主串当前字符比较,其他位置前没有相同的两端也是这样。在图中可以看到,相同的两端如果只有一个字符,next[j] = 2
如果像下图这样
kmp
失配位置之前的相等的两端有两个字符,那么 next[j] = 3,规律就是,next[j] = 相等的字符数 + 1;
考虑代码:

void getnext(String T, int next[])
{
	int i = 1; int j = 0;
	next[1] = 0;
	while (i < T[0]) {
		if (j == 0 || T[i] == T[j]) {
			++i;
			++j;
			next[i] = j;
		}
		else j = next[j];
	}
}

刚开始就初始化 next[1] = 0;进入 while 循环时,j = 0,那么得到 next[2] = 1;
假设任何位置前都没有相等的两端:若T[2] != T[1],进入 else 分支,j = next[1] = 0;然后又进入 if 条件体,next[3] = 1;接着又j = next[1] = 0,next[4] = 1…
假设从第3位前都是相等的两端:T[2] = T[1],那么有next[3] = [2],假设又T[3] = T[2],又有 next[4] = 3…会像下图这样
kmp
有的位置之前的两端相等,其他位置之前的两端又不相等的情况自己验证代码,都是满足的

KMP模式匹配的代码如下

int Index_KMP(String S, String T, int pos)
{
	int i = pos; int j = 1;
	while (i <= S[0] && j <= T[0]) {
		if (j == 0 || S[i] == T[j]) {
			++i;
			++j;
		}
		else j = next[j];
	}
	if (j > T[0]) return i - T[0];
	else return 0;
}

和最开头的暴力匹配代码几乎一样,就是同暴力匹配回溯的位置不同,KMP算法不回溯 i,对 j 的回溯也不是单纯回溯到 j = 1,它的时间复杂度最差情况下都是线性的,相当于暴力匹配中每次都是第一个字符失配的情况。

其实KMP算法还可以改进:考虑下图的情况。
kmp改进
之前我们不会去考虑失配的这个字符是什么,按照它前面串两端相等的字符数推出next[4] = 3;如果 T[4] = T[3],那又拿 T[3] 和主串的这个字符比是没有意义的,应该直接掠过所有和 T[j] 相等的 next[j] 字符
getnext 函数改写如下

void getnextval(String T, int nextval[])
{
	int i = 1; int j = 0; nextval[1] = 0;
	while (i < T[0]) {
		if (j == 0 || T[i] == T[j]) {
			++i;
			++j;
			if (T[i] != T[j]) nextval[i] = j;
			else nextval[i] = nextval[j];
		}
	else j = nextval[j];
	}
}

以上代码的逻辑不难理解,就是省去 T[j] = T[next[j]] 这种情况的比较,当这种情况发生时,再求一下 next[next[j]] 就是了。

运行结果如下
1.getnext版KMP
在这里插入图片描述
2.getnextval版KMP
在这里插入图片描述
结果均正确,由于测试的串很短,都是瞬间匹配完成。

模式匹配算法就介绍到这里,如果觉得不好理解就多在纸上模拟两遍,其实逻辑并不很复杂。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值