【叶子算法系列1】——字符匹配KMP算法

声明

鉴于当前平台上很多资料鱼龙混杂,往往给新手学习带来极大的不便,更有甚者,根本知识就是错的,误导新人!很多作者的代码根本就跑不通,还煞有介事地大加分析,似乎很是有些道理,却给新手带来了很坏的影响。本着严谨学术的态度和开放的计算机科学思想,也顺便梳理自己的笔记,因此把学习的过程和思考记录了下来。作者本人水平有限,尽管尽力做到严谨,但仍不可避免存在纰漏,因而也欢迎大家提出质疑、批评和指正。
这个系列是本人学习笔记,适合有一定基础的人加深对于算法思想的理解。如不了解KMP算法,请先学习KMP的基本知识。

原理

朴素的字符串匹配算法基于简单的字符比较结合文本和模式的对应指针(i,j)依次回溯实现。KMP本质是一种基于DP思想的算法,通过next数组记录模式的最长前缀子串及其位置,从而在不回溯 i 的前提下快速回溯模式的对应指针 j 实现匹配比对,减少了指针回溯的次数。

思路

算法的具体思想很朴素:一般来说,当字符匹配失败时我们需要回溯指针 i, j 到下一个位置重新匹配。那么问题来了,在这个字符匹配失败发生时(假设为第k位),前 k-1 位的字符是匹配的,两个指针分别需要回溯到哪里呢?
因为前 k-1 位的字符是匹配的,因此匹配失败时,保持 i 不动,回溯指针 j 继续比对就可以了。于是问题就变成了当匹配失败的时候,下一个 j 的位置应该在哪?

。。。等一下,似乎有些地方不对,我们怎么确保在不回溯的文本段中没有匹配的情况呢?

  • 证明: 设文本串为s,模式串为 p。 假设第 k 位匹配失败,则前 k-1 位相同。采用朴素匹配算法,假设此时模式串后移一位仍可以正确匹配,则
    s[0-(k-1)] = p[0-(k-1)], 又有 s[1-k] = p[0-(k-1)], 即 s[0-(k-1)] = s[1-k] , 由归纳法可知,当且仅当 s[0]=s[1]=…=s[k] = p[0]=p[1]=…=p[k-1] 时,假设成立。

看来尽管情况很特殊,但不排除存在这种情况。先不急,慢慢看我们能否解决。

上面讲到关于 j 的位置的问题,可以用next数组来解答。next数组保存了模式串中最长前缀的位置,当匹配失败的时候告诉 j 当前已匹配串中最长前缀的下一个对比的位置。

  • 最长前缀: 一个串中不包含最后一个字符的前缀列和不包含第一个字符的后缀列的最大交集。

那么next要怎么求呢?

若有模式s:

sABACABAB
next-10010123

这里直接给出next的结果。
首先定义next[0]=-1,next[1]=0。因为第一个字符的没有子串,故定义为-1;第二个字符对应子串(‘A’)的前后缀为空,因此为0。
第三个字符对应子串(‘AB’)的前缀为A,后缀为B,交集为空,故next[2]=0。
第四个字符对应子串(‘ABA’)的前后缀交集为A,故next[3]=1。
第五个字符对应子串(‘ABAC’)的前后缀交集为空,故next[4]=0。
第六个字符对应子串(‘ABACA’)的前后缀交集为A,故next[5]=1。
第七个字符对应子串(‘ABACAB’)的前后缀交集为AB,故next[6]=2。
第八个字符对应子串(‘ABACABA’)的前后缀交集为ABA,故next[6]=3。

这里重点关注几个位置。
next[5]=1。求这一步时,我们比对了 s[4] 和 s[0] ,得出最长前缀是1(显然我们的结论是正确的)。然后求next[6],这时我们比对了 s[5] 和 s[1] ,得出最长前缀是2(即1+1),由于前一步已经表明存在一个字符长度的前缀,因此这一步直接就可以从第二个字符开始比对,也就是说我们可以利用先前得到的信息计算下一个值,而不必从头开始(这就是DP!)。
next[4]=0。求这一步时,我们比对了 s[3] 和 s[1] ,发现没有匹配,这时我们就要往回找前一个最长前缀的位置next[1]=0,(为什么?因为这里没有匹配说明这个前缀不可能是最长前缀了,因此回溯指针 j )然后比对 s[3] 和 s[0] ,发现没有匹配,这时next[0]=-1,无法回溯,因此next[4]=0。

因此next数组的求解用代码就可以表示为:

int* get_next(string pattern) {
	int next_len = pattern.length();
	int *next = new int[next_len];
	//初始化
	int j=2, k;
	next[0] = -1;
	next[1] = 0;
	k = next[j - 1];//已知最长前缀的下一个字符位置

	while (j < next_len)
	{
		if (pattern[j-1] == pattern[k])//从子串的最后一个字符开始比对
		{
			next[j] = next[j - 1] + 1;//DP思想
			j++;
			k++;
		}
		else
		{
			if (k == 0)//第一个也对不上,说明j应该右移
			{
				next[j] = 0;
				j++;
			}
			else
			{
				k = next[k];//往回找前一个最长前缀的位置
			}
		}
	}
	return next;
}

那么next数组的实质就很明显了:一个包含所有可能失配的位置对应可以绕过最多无效字符的列表。

好的,是时候来看看上面的特殊情况了。
当满足特殊情况时,next 如下

index0k-1kk+1
next-1k-2k-1

(思考一下为什么next是这样的?)
k 失配后,j = next[k] = k-1,此时 i = k,说明s[1-k] = p[0-(k-1)], 也就是说,next数组包含了这种特殊情况,因此算法是正确的。

从而得到KMP算法:

int KMP(string String, string pattern) {
	//
	int *next = get_next(pattern);
	int i = 0, j = 0;

	while (i < String.length() && j < pattern.length())//
	{
		if (String[i] == pattern[j])
		{
			i++;
			j++;
		}
		else
		{
			if (j == 0)//第一个也对不上,说明i应该右移
			{
				i++;
			}
			else
			{
				j = next[j];//往回找前一个最长前缀的位置
			}
		}
	}
		if (j == pattern.length())
		{
			return i - j;//返回匹配位置
		}
		else
		{
			return -1;//返回错误
		}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值