KMP算法是怎么被设计出来的

定义

我们假设要在主串中寻找子串出现的所有位置
我们记主串中的开始位置为匹配位置,如在 “abc” 中匹配 “bc” ,则匹配位置为 (2)

暴力

我们把匹配过程拆解为

  1. 枚举匹配位置
  2. 验证主串从匹配位置开始是否一一匹配子串

以此,有显然的 O ( n m ) O(nm) O(nm) 算法

基于优化推出KMP

主串:ABCABCABD
子串:ABCABD

  1. 枚举匹配位置 (1),已经验证了ABCAB是匹配的,现在发现C和D匹配不上
  2. 显然我们该要枚举下一个匹配位置,但是我们其实已经在刚刚的验证过程中知道了下一个匹配位置 (2) 的前几位就是 BCAB,同理下下位 (3) 就是 CAB,这些一定会和已经看到了的 ABCAB 就匹配失败的,我们有没有办法直接让他跳到最近的下一个可以匹配得上的位置,也就是 (4) AB 呢。
  3. 我们考虑思考匹配位置从 (i) 变到 (i+1) 意味着什么
    对于主串来说:匹配位置右移,我们将看到的主串就等于已经匹配的 ABCAB 靠左切掉一个字母 (ABCAB → \rightarrow A|BCAB)
    对于子串来说,匹配位置右移,相当于已匹配长度少了 1,也就是 ABCAB 靠右切掉一个字母 (ABCAB → \rightarrow ABCA|B)
    此时匹配位置向右挪 x 位能否匹配就相当于问,已看到的 ABCAB 左边去掉 x 位是否和右边去掉 x 位相同,因为我们要按顺序找到下一个最近的匹配位置,所以最小的 x (x>0) 等于在问,最长前后公共缀,然后总长度减它就是 x
    前后公共缀:同时是串的前缀和后缀,如ABACABA的前后公共缀有A,ABA,(ABACABA本身也是,但是x不为0,相当于一定要删点东西,所以我们不考虑串本身)
  4. 我们考虑把枚举下一个匹配位置想象成主串不动,子串和它的头向后拖,然后 nxt[i] 直接记作 i 失配要跳到哪个位置比如
    主串 ABCABCABD
    子串 ABCABD,此时 i=6 , j=6 失配,因为nxt[6]=3
    -> 跳到 i=6 j=3
    主串 ABCABCABD
    子串         ABCABD
  5. 显然存在一种情况就是没有不是本身的前后公共缀,如 ABC
    主串 ABCABCD
    子串 ABCD       此时 i=4 , j=4 失配
    就说明匹配位置 (1) (2) (3) 都是不可能的
    那么下一个匹配位置就是 nxt[4]=1
    主串 ABCABCD
    子串         ABCD 此时i=4 , j=1
  6. 如果匹配成功,到达子串最后一位了,我们假假地认为子串结尾有一个唯一存在的字符,那么再匹配一位就相当于又失配了,所以我们可以定义出 nxt[len+1],它取决于整个子串的最长公共前后缀

求 nxt 数组

求 nxt 数组的过程可以先想象成对子串本身的一次 kmp

  1. 先以子串 ABACABA 为例
    假设要求nxt[7],已经匹配出nxt[6]的最长公共缀是ABACABA,那么加入最后一位 A 之后,发现这时候能接着匹配上,那么得到 nxt[7]=nxt[6]+1
  2. 如果匹配不上,如 s = “ABACABAB” ,要求nxt[8],已经匹配出nxt[7] ABACABAB,加入的 B 和前面的 C 失配,那么问题就变成了求前面的 ABA 靠右切 x-1,后面的 ABAB 靠左切 x,相等的最小 x,所以我们又变成了求 ABAB 的最长公共缀,这里 ABA 是 nxt[7] 已经匹配的,然后再加上第 8 位 B,而这又相当于把s[4]替换成B,所以我们变成了求 s[4]=B 时的 nxt[4],不难发现继续下去我们得到了一个递归结构,边界就是跳到头,那么就说明不存在

代码实践

相同的思想,不同的实现之间有比较大的差别,以下给出最简练的代码和解释

示例代码的下标从0开始记
不难发现nxt天然即和下标有关,又和公共缀长度有关,是相等还是差1取决于下标起点

/*求nxt,s2是子串,m是子串长度*/
nxt[0]=nxt[1]=0;
for(int i=1;i<=m;i++){
	int j=nxt[i];
	while(j && s2[j]!=s2[i]) j=nxt[j];
	//s2前后相同i+1就记j+1了,不同则j=nxt[j],递归成子问题
	nxt[i+1]=s2[j]==s2[i]?j+1:0;
	//nxt=0表示不存在公共缀,从头开始
}

/*求匹配位置并输出*/
for(int i=0,j=0;i<n;i++){
	while(j && s2[j]!=s1[i]) j=nxt[j];
	if(s2[j]==s1[i]) j++;
	if(j==m){
		printf("匹配位置:%d\n",i-(m-1));
		j=nxt[j];
	}
}

时间复杂度


图片来自"云端潜行" 遵循 CC 4.0 BY-SA 版权协议
这张图展示了KMP的匹配过程,从代码上看,最令人担心的就是每个 for 内部的 while,我们极端化,while 每次都 while 到头,情况如上图,那么我们发现每次 while 中 j j j 回退的次数最多是这个周期中 i i i 增加的个数,所以 j j j 改变量的和不超过 i i i 改变量的和,所以加起来是小于 2 N 2N 2N
nxt 数组和 KMP 匹配的过程类似

总结

kmp 的后半部分就是在失配后,充分利用前面已经验证出来的匹配部分来减少试错
kmp 的前半部分求 nxt 也差不多

改进

nxt 的设计上还有一种改进,假设子串 abaabaaba
nxt[6] 正常是 3,但是我们可以发现s[3]=s[6],所以 6 失配,3 一定也会失配的,所以我们直接把nxt[6] 指到 nxt[3]
同理我们发现 s[9]=s[6] , 所以 nxt[9] 本来是指到 6 的,现在指到 nxt[6] ,因为 nxt[6] 又指到了 nxt[3],所以结果是 nxt[9]=nxt[3]
这种改进是基于我们在观察失配时只考虑了失配前的已匹配串,但我们其实可以把失配的那个位置的字母也考虑进去。如果我们不处理这一优化,kmp 进行时会自己一路跳下去,如果这种情况出现多次,那么我们提前处理就会加速。
经测试,对于随机数据,可以加速 5-10%

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jarden_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值