也许是全网最清晰易懂的KMP讲稿?——operator_


说实话现在网上的 K M P KMP KMP 讲稿对于我这样的蒟蒻还是太不友好了,里面基本上只写到 怎么做 ,但不告诉你 为什么这么做是对的 ,所以理解起来会有点云里雾里,其实只要理解了本质你会发现 K M P KMP KMP 其实很简单。

暴力匹配

我们用 T = a a b a a c T=aabaac T=aabaac 去匹配 S = a a b a a b a a c S=aabaabaac S=aabaabaac 作例子(下标都是从 1 1 1 开始的)。

第一次匹配:

第二次匹配:

第三次匹配:

第四次匹配(找到了):

我们在四次匹配中总共作了 15 15 15 次比较(因为匹配失败也需要一次比较),暴力的最坏复杂度是 O ( n m ) O(nm) O(nm)

优化:用失配指针匹配

我们考虑如何优化。

我们发现,在暴力匹配时,有很多比较是多余的,比如说,当我们进行了第一次匹配后,第二次匹配其实完全是多余的,因为第二次匹配成功的 必要条件 S 2 − 5 = T 1 − 4 S_{2-5}=T_{1-4} S25=T14 ,然而在第一次匹配时我们得知了 S 2 − 5 = T 2 − 5 S_{2-5}=T_{2-5} S25=T25 ,而 T 1 − 4 ≠ T 2 − 5 T_{1-4}\not=T_{2-5} T14=T25 ,所以我们不用比较 S S S 也能知道 从下标 2 2 2 开始的匹配不可能成功 我们只需要对 T T T 进行预处理。

再比如,当我们进行了第一次匹配后,第四次匹配的前两次比较其实也是多余的,因为我们通过第一次匹配我们得知了 S 4 − 5 = T 4 − 5 S_{4-5}=T_{4-5} S45=T45 ,而 T 1 − 2 = T 4 − 5 T_{1-2}=T_{4-5} T12=T45 ,所以我们进行第四次比较时可以直接从 T 3 T_3 T3 开始。

观察一下 T 1 − 4 ≠ T 2 − 5 T_{1-4}\not=T_{2-5} T14=T25 T 1 − 2 = T 4 − 5 T_{1-2}=T_{4-5} T12=T45 这两个式子有什么共同点?发现一个两边的串一个是 T 1 , k T_{1,k} T1,k 的前缀,另一个是 T 1 , k T_{1,k} T1,k 的后缀,上述两式中 k k k 都等于 5 5 5 (如果看不出来可以多举几个例子)。

(这里的 后缀 也是顺序的,指的是包含最后一个元素的连续一段, 不是逆序的!!! 不要像我一样傻憨憨的以为是倒过来的)

你应该已经有思路了,我们如果能对 T T T 串处理出 所有前缀的最长公共前后缀长度 (比如上例中 T 1 − 5 T_{1-5} T15 的最长公共前后缀长度是 2 2 2 ),好像就可以完成上面的两个优化了。

怎么写程序呢?其实不用很多判断,我们维护两个指针 i , j i,j i,j i i i 指向 S S S 中当前比较到哪里, j j j 指向 S S S 中当前比较的位置的 前一个 i i i 指针不断 + 1 +1 +1 就好了,对于 j j j ,我们看看下一个要比较的字符是否与 i i i 的字符相同,相同就 + 1 +1 +1 并比较下去,不同就跳到当前位置的 失配指针

等等,什么是失配指针?顾名思义,就是如果在当前位置失配了下一个要比较哪里。那怎么求?这个东西其实就是上文的前缀的最长公共前后缀长度,原因如下:

首先跳过去一定是对的,上文已经解释过了。其次,跳过去中间不会漏下任何情况,看我们最开始的例子,第二次匹配为什么是不必要的?因为 T 1 − 4 T_{1-4} T14 一定与 T 2 − 5 T_{2-5} T25 不同,如果这两个串相同,就违反了我们 最长 公共前后缀长度的定义,所以不重不漏。

综上,跳到失配指针一定不会有问题。

代码如下:

int nxt[];//失配指针
int kmp(char s[],char t[]) {
	get(t);//求失配指针,待会再讲
	for(int i=1,j=0;i<=strlen(s+1);i++) {
		while(j&&s[i]!=t[j+1]) j=nxt[j];//跳失配指针
		if(s[i]==t[j+1]) j++;
		if(j==strlen(t+1)) return i-j+1;//找到了返回位置
	}
	return -1;
}

你可能有疑问,我们在跳失配指针时看起来是有一个内循环的,复杂度正确吗?

事实上当然是正确的,原因在于 j j j 每跳一次都会变小,又大于 0 0 0 所以最多跳 j j j 次,然而 j j j 唯一能变大的地方是 if(s[i]==t[j+1]) j++;这个语句最多运行 n n n 次,因此 j j j 最多变大 n n n 次,所以跳失配指针也最多跳 n n n 次,复杂度正确。

求失配指针

最后我们来考虑求失配指针。

这就很神奇了,结论是:我们 T T T 去匹配 T T T 自己 就好了。

代码如下:

void get(char t[]) {
	nxt[1]=0;
	for(int i=2,j=0;i<=strlen(t+1);i++) {
		while(j&&t[i]!=t[j+1]) j=nxt[j];
		if(t[i]==t[j+1]) j++;
		nxt[i]=j;
	}
}

这为什么是对的呢?在我们执行 nxt[i]=j;语句时,其实就保证了 T 1 − j T_{1-j} T1j 匹配成功了 T 1 − i T_{1-i} T1i ,且包含了 T i T_i Ti ,所以 T 1 − j T_{1-j} T1j 就是 T 1 − i T_{1-i} T1i 的最长公共前后缀(看不懂再去理解一下匹配 S S S 的那个函数)。

完整代码如下:

int nxt[];
void get(char t[]) {
	nxt[1]=0;
	for(int i=2,j=0;i<=strlen(t+1);i++) {
		while(j&&t[i]!=t[j+1]) j=nxt[j];
		if(t[i]==t[j+1]) j++;
		nxt[i]=j;
	}
}
int kmp(char s[],char t[]) {
	get(t);
	for(int i=1,j=0;i<=strlen(s+1);i++) {
		while(j&&s[i]!=t[j+1]) j=nxt[j];
		if(s[i]==t[j+1]) j++;
		if(j==strlen(t+1)) return i-j+1;
	}
	return -1;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值