让人头大的KMP算法

KMP算法:

  • 什么是kmp?
    给定两个字符串,分别为 str1 和 str2 ,问:在 str1 中,是否存在子字符串和 str2 相等?

简单暴力法

  • str1 从头开始遍历,如果当前字符和 str2 中第一个字符相等,则判断 str1 下一个字符和 str2 第二个字符是否相等,如果相等,继续判断第三个、第四个、、,如果不相等,那么 str2 又要回到开头位置,等待下一次, str1 也要往回走。(很简单的道理,就不在赘述)

重点来了----KMP

首先kmp算法借助了一个 next 数组,这个数组和 str2 长度相同,其中每个位置的值表示 str2 在当前位置,它的前一个字符的前缀和后缀相等的最大长度。这么说可能有点绕,举个例子:

  • 假如现在 str2 = ‘ABCDABD’;

那么对于 str2[5] = ‘B’ 这个位置,它的前一个字符(即 str[4] = ‘A’)的所有前缀包括(默认前缀不包括最后一个字符即这里的 ‘A’):

'A', 'AB', 'ABC', 'ABCD'

所有后缀包括,默认后缀不包括第一个字符,即 ‘A’:

'A', 'DA', 'CDA', 'BCDA'

在所有前缀和后缀中,相等的只有 ‘A’ ,因而前缀和后缀相等的最大长度为1,所以相应的在next数组中,next[5] = 1;

再来一个,在 str2[6] = ‘D’ 处,它的前一个字符(‘B’)位置的所有前缀为:

'A', 'AB', 'ABC', 'ABCD', 'ABCDA'

所有后缀为:

'B', 'AB', 'DAB', 'CDAB', 'BCDAB'

前缀和后缀中两两相等的只有 ‘AB’,所以最大长度为2,故 next[6] = 2;

算法流程:

  1. 初始化 next 数组,默认设置 next[0] = -1, next[1] = 0, 长度和 str2 一致;
  2. 设置初始跳跃位置 cn = 0, 从下标 i = 2 开始,遍历求解整个字符串;
  3. 判断前一个位置的字符与 cn 位置上的字符是否相等,相等则 next[i] = cn + 1,同时 cn + 1
  4. 如果不相等,再判断 cn > 0 ?,大于 0 的话表示 cn 不在 0 位置和 1 位置上,还可以继续往回跳;
  5. 如果不大于 0 ,则表示 cn 不能再回头跳,只能当前 next[i] = 0;

解释一下 cn 是什么情况,整个求解 next 数组的过程说起来手累心更累。硬着头皮试一下吧,从 i = 2 开始,递推一下整个过程;

  • str2 = ‘ABCDABD’, next[0] = -1, next[1] = 0, cn = 0;
    当 i = 2 时, 由于 str2[1] != str2[0], 而且 cn == 0 ,所以 next[2] = 0,一直到 i = 5,str2[cn] = str2[4], cn = cn + 1, next[5] = 1, 继续 i = 6 时,此时 cn= 1, 而 str2[2] = str2[5] ,所以cn = 2, next[6] = 2。
    • 在这个求解过程中,cn 的值就表示前缀和后缀相等的最大长度,而前缀又是从0开始的,所以这个长度就等同于位置信息,就是说 cn 的值同时也代表 0 ~ cn - 1(不是 cn ,因为位置从 0 开始)是当前位置的前一个字符前缀和后缀相等中的最长前缀。
    • 当我判断前一个位置的字符和 cn 位置的字符是否相等时,因为 cn 已经表示在前一个字符这个位置之前的前缀和后缀相等的最大长度信息,那我整体往前继续判断的时候,就不需要重新再判断之前的信息了,就好像动态规划一样,利用之前已经判断过的信息。
    • 因此如果两者相等,那就意味着前缀和后缀相等的最大长度又可以 + 1 了,不相等的话,只要前面还存在着前缀和后缀相等的情况,那我就继续判断上一次前缀和后缀相等时的cn 的信息,直到没有这个信息为止。(整个过程说起来着实不容易,强烈建议手推一下,才能理解)

高潮部分

利用 next 数组信息,只需要遍历一次 str1 字符串,就可以完成判断;

算法流程:

  1. 从头开始判断,i, j = 0, 0 ,如果str1[i] == str2[j],同时自加1,继续下一个字符;
  2. 如果不相等, 看next[j] == 0?如果是,说明 str2 的第一个字符就没有匹配成功,那 str1 只能继续往下走;
  3. 如果不相等,且next[j] != 0, 说明前面还存在着相等的重复区域,也就是还存在着前缀和后缀相等的情况,那我就可以借助这个信息避免重新往回走判断;
  4. 当两个字符串中有一个已经遍历到头了,过程终止,此时结果已经出来了。如果 j == str2的长度,说明在遍历的时候,我在 str1 中遇到了和它一样的子串,返回这个子串的起始位置就是 i - j;如果不是,那就说明没有遇见和它一样的子串。

整个过程虽然看起来复杂,但代码其实很简单,关键是懂得如何利用之前的信息,做到不重复判断。

附上代码:

def bmp(str1, str2):
    if len(str1) == 0: return 0
    if len(str2) == 0: return 0

    i, j = 0, 0
    nextArray = getNextArray(str2)
    while (i < len(str1) and j < len(str2)):
        if str1[i] == str2[j]:
            i += 1
            j += 1
        elif nextArray[j] == -1:
            i += 1
        else:
            j = nextArray[j]
    return i - j if j == len(str2) else -1

def getNextArray(str2):
    if len(str2) == 1:
        return [-1]
    res = [0] * len(str2)
    res[0] = -1
    cn = 0
    for i in range(2, len(str2)):
        if str2[i - 1] == str2[cn]:
            cn += 1
            res[i] = cn

        elif cn > 0:
            cn = res[cn]
        else:
            res[i] = 0
    return res

另外:https://blog.csdn.net/v_JULY_v/article/details/7041827,这个链接中求next数组的方式和我的方法有点差别,我是根据牛客网左程云的算法讲解视频中内容实现的,个人感觉左程云讲的更好一点,有条件的可以去看看他的视频。

注:讲清楚这个算法实在是太难了,,写了一天也没写明白,尽力了,日后如果想到更简单的叙说方式再回来补吧!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值