KMP算法

今天复习了一下KMP算法,发现忘得差不多了,只能说当时理解还是不够,所以整理了一下,换一个思路来理解KMP算法。

KMP算法主要的思路是对pattern串进行预处理,得到一个数组next数组,next[i] 表示 当pattern[i]与目标串target[j]失配时,应将pattern串中的哪个元素直接移动到与目标串target[j]匹配,而不是一个一个向右移去匹配。其原理是,在我们得到next[i]失配时,我们知道next[0...i-1]是匹配的,亦即target[j-i...j-1]是已知的,而预处理阶段target[j]是未知的。

顺着这个思路,我们不妨假设pattern[0...k-1]与target[j-k...j-1]是匹配的,显然k<i,而在这个假设下,我们应该直接将pattern[k]与target[j]对齐。

那也就是说 pattern[0...k-1] = target[j-k...j-1]  = pattern[i-k...i-1],这意味着什么,我们不妨看两个例子

i012345678
pattern[i]aabbaabba

i012345678
pattern[i]aabcdefaa

对第一个例子,有从pattern[0]开始的aabba和从pattern[4]开始的aabba(或者应该说以patter[8]结尾的)

第二个例子则是,从patter[0]开始的aa和以patter[8]结束的aa。


到这里我们可以回顾一下,顺便考虑一些细节。为了在patter[i]失配的时候,我们要找到一个好的匹配位置k,使得pattern[0...i-1]有一个长度为k的前缀和长度为k的后缀相同(这里的前缀后缀都不包括自身)。这里有几个细节需要注意一下。一个是,我们的的匹配位置k是不包含在前缀中的,因为它要与target[j]对齐;另一个则是,我们这里选取的长度k应为所有可行值中最大的,只有这样才不会出现遗漏。


那接下来就是如何求解next数组。

这里我们采用next数组的前缀后缀含义来理解:next[i] = k,k是pattern[0...i-1]相同前缀后缀的最大长度(这里的前缀后缀都不包括自身)

不如先考虑一下初始情况

(1)i=0 时 next[0] = -1

当pattern[0]与target[j]失配时,我们应将pattern整体右移一位,可以视为我们将pattern[-1]与target[j]对齐

(2)i=1时 next[1] = 0

当patter[1]与target[j]失配时,显然我们应将patter[0]与target[j]对齐

下面我们来看i>0的一般解

此时pattern[0...i-1]匹配,而pattern[i]失配,考虑pattern[0...i-1]

(1)如果pattern[i-1]  =  pattern[next[i-1]]

显然此时 next[i]  =  next[i-1]+1

(2)如果pattern[i-1]  !=  pattern[next[i-1]]

我们先来看一个例子

i0123456789
pattern[i]abbaabbabd

若此时i=9,可以知道next[i-1] = next[8] = 4,而pattern[8]  !=  pattern[4]

抽象一点来看这个问题,pattern[8]  与  pattern[4] 不相同时,可以肯定的是,next[9] < next[8](我们所求的那个前缀必定短于4)。也就是说,我们所求的那个前缀,也是pattern[0...next[8]-1]的一个前缀;而我们所求的前缀对应的后缀,是pattern[8-next[8]...8]的后缀。我们注意到,如果pattern[next[8]]  = pattern[8],我们所求的那个后缀,也会是pattern[0...next[8]]的一个后缀。综上,如果pattern[next[8]] = pattern[8],我们所求的pattern[0...8]的前缀,也就是pattern[0...next[8]]的前缀,而这里就是不断递归减小问题规模。

接下来我们考虑一下更实际一点的问题。

第一个问题是,我们不能真的改变pattern中的值。

这个问题很好解决,例如例子中,我们只要每次比较都使用pattern[8]作比较即可,详细可看代码。

第二个问题是,递归的边界。

显然,当我们发现pattern[i-1]  =  pattern[next[i-1]]时可以退出。另一个边界则应该是k=-1,也就是说pattern[0]都不等于pattern[i-1],前缀长度为0


理解了上面的东西,我们就可以写求next数组的代码了,这里我就直接贴一份书上的代码了,当然里面涉及到了一点优化,代码也更整洁(也更难懂一点)


int *findNext(String P)
{
	int i=0;
	int k=-1;
	int m=P.length();
	assert(m>0);			//若m<=0则退出
	int *next = new int[m];
	assert(next !=0)		//申请空间失败则退出
	next[0] = -1;
	while(i<m)							//循环中计算的是P[0...i-1]的最大相同前缀后缀长度(i-1是相对于next[i]赋值表达式中的i来说)
	{
		while(k>=0 && P[i]!=P[k])
			k = next[k];
		k++;
		i++;
		if(i==m) break;
		if(P[i]==P[k])
			next[i] = next[k];
		else	next[i] = k;
	}
}

int KMPStrMatching(const String &T,const String &P,int *N)
{
	int i=0;
	int j=0;
	int pLen = P.length();
	int tLen = T.length();
	if(tLen<pLen)
		return -1;
	while(i<pLen && j<tLen)
	{
		if(i==-1 || P[i]==T[j])
		{
			i++;
			j++;
		}
		else i = N[i];
	}
	if(i>=pLen)
		return j-pLen+1;
	else
		return -1;
}

(如果您发现了任何的问题,请务必告知我,十分感谢)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值