KMP算法思路拆分

KMP算法

  KMP算法是一种字符串匹配算法,这里字符串匹配是指:“字符串P是否为字符串S的子串(后面统一用P表示模式串,S表示主串),如果是,它出现在S的哪个位置(起始位置)”,KMP算法可以在O(n+m)的时间里实现这样的匹配。接下来我会以通俗易懂的方式,一步一步讲解这个算法的思路。
  首先,我们来看字符串匹配这个问题,最直接的解决方式就是暴力破解,这个非常容易理解,从主串S的第一个字符开始,和模式串P逐个字符进行比较,直到出现不匹配的情况,然后再继续从S的第二个字符开始,重新和P逐字符比较,依次类推,直到找到完全匹配的位置或者可能直到S尽头也没有完全匹配(当然这里完全不用尝试到S串的尽头,只要剩下的S串长度小于P串长度,就可以终止匹配了)。
  之所以介绍暴力破解这种方式,是因为KMP算法就是基于这种方式做了一些改进。在正式介绍KMP算法之前,先举一个例子:S =“ababa…”,P =“ababe”, i 表示主串S的下标, j 表示模式串P的下标,下标从0开始。通常情况下,我们人为判断一般也是从主串S起始位置开始,当匹配到第五个字符,即i = j = 4时,匹配失败。在这个时候我们往往不会,从S的下一个字符进行判断,因为在第一次匹配过程中我们已经得到了一些信息,根本没必要从S的第二个字符再重新判断,至少从第三个字符开始判断才有可能匹配成功,所以我们可以直接略过S的第二个字符,从S的第三个字符开始重新判断,即从 i = 2 , j = 0的初始条件开始判断……与暴力破解不同的是,我们利用在匹配过程中收集到的信息,排除了一些必然不可能的选项,减少了匹配次数。这个模式大致流程是这样的,逐步移动 i 和 j 进行比较,若出现不匹配的情况, j 重置为0,而 i 则根据匹配过程中收集到的信息,跳过一些不可能的选项,即 i 不再是回到本次匹配过程中起始位置的下一跳了,可能直接到下下个字符,或者更多。这种方法通常是我们人为判断的一种方法,但并没有将匹配过程中收集到的信息利用完全。还是以上面那对字符串为例,我们明明已经匹配到了S的第五个字符,第二次匹配的时候却还是从第三个字符开始重新判断,这就意味着,第四和第五个字符的信息我们浪费掉了,我们没有物尽其用。那我们稍微用心一下,就会发现,第一次匹配失败后,我们可以直接从 i = 4 , j = 2 的位置重新开始判断。重点来了,第一次匹配中,S串和P串有4个相同的字符串,即S[0 : 3] = P[0 : 3],那么一定有S[2 : 3] = P[2 : 3];第二次匹配,S串从第五个字符开始,P串从第三个字符开始,这意味着,P串前两个字符和S串的第三、第四个字符是相同的,即S[2 : 3] = P[0 : 1],最终我们得到一个结果P[0 : 1] = P[2 : 3],即P串的前两个字符和P串的第3至4个字符是一样的,也正因为他们一样,我们才能在第二次匹配的时候直接从 i = 4 , j = 2 开始匹配。从这里我们开始引入KMP算法第一个重要的定义:前缀和后缀,定义 “k-前缀” 为一个字符串的前 k 个字符; “k-后缀” 为一个字符串的后 k 个字符, k 必须小于字符串长度。在第五个字符匹配失败之前已经成功匹配了4个字符"abab",P[0 : 1]和P[2 : 3]恰好是这个字符最长的相等的前后缀,我们定义为真前后缀,其他字符串相等的前后缀统称为公共前后缀。
在这里插入图片描述
  当在某一位置匹配失败后,我们只需要获取该字符在P串位置前的字符串的真前后缀,然后将当前真前缀对准真后缀的位置就可以了。也就是说,我们可以不回溯 i ,只移动下标 j ,就能完成字符串的匹配,即当 j = k 时发现字符不匹配,本轮次匹配结束,然后我们求P串前 k-1 个字符的真前后缀,将下标 j 移动到真前缀的下一个位置即可,这种匹配方式尽可能地使用到了已匹配过的信息,这就是KMP算法的匹配方式。
  现在问题来了,我们如何求这个真前后缀呢,这就要引入KMP算法的第二个概念next[]数组,next[i]表示P[0 : i]的真前后缀的长度,next[0] = 0。举个例子:"ababe"对应的next[] = [0,0,1,2,0]。为什么next[i]表示P[0 : i]而不是S[0 : i]的真前后缀的长度?因为在匹配过程中,我们利用的是已经匹配过的信息,那么已经匹配上的字符,表示S串和P串同时拥有这些字符,参照其中一个即可,所以只需要要求出P串的next数组,就可以将所有可能出现的情况囊括其中(不用关注S串)。
  next[]数组咋求?可以采用递归的方法,已知next[i] = k,即P[0 : i]的真前后缀长度为k,即P[0 : k - 1] = P[i - k + 1 : i]。如果P[k] = P[i + 1],则P[0 : k - 1] + P[k] = P[i - k + 1 : i] + P[i + 1],即P[0 : k] = P[i - k + 1 : i + 1],则next[i + 1] = k + 1。若P[k] != P[i + 1],此时next[i + 1] != k + 1。首先next[i + 1]一定是小于k + 1的(易证,反证法即可),那么此刻我们如何找到新的最大公共前后缀?这里依然要用到next[i] = k这个信息,P[0 : k - 1] = P[i - k + 1 : i]。首先假设next[i + 1] = j,则有 j < k + 1,P[0 :j - 1] = P[i + 1 - j + 1 : i + 1],P[0 :j - 2] = P[i + 1 - j + 1 : i],next[i] = (j - 1) + 1,我们需要做的就是找个某个 m(可能有多个,那就找到最大的那个) 使得next[m] = j - 1 ,且有P[j - 1] = P[i + 1]。又因为P[0 : k - 1] = P[i - k + 1 : i],j <= k,即P[i - k + 1 : i]包含P[i + 1 - j + 1 : i],所以P[i + 1 - j + 1 : i]是P[i - k + 1 : i]的后缀,所以P[i + 1 - j + 1 : i]也是P[0 : k - 1]的后缀,P[0 :j - 2]是P[0 : k - 1]的前缀,所以P[i + 1 - j + 1 : i]和P[0 :j - 2]一定是P[0 : k - 1]的公共前后缀。一个字符串有多个公共前后缀和一个真前后缀(最大公共缀),那么这些公共前后缀一定也是这个真前后缀字符串的公共前后缀,证明思路与上述证明过程雷同。所以这就提供了一个思路,如果P[k] != P[i + 1],我们先检索当前已匹配的字符串的最大公共前后缀,然后判断其前缀后面的一个字符是否和P[i + 1]相等,如果相等就按照第一步来,如果不等,就继续找这个前缀的最大公共前后缀(第二大的公共前后缀),依次循环下去,直到出现相等的情况或者前后缀长度为0的情况(next[i + 1] = 0)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值