KMP 算法

KMP 算法主要是用于解决字符串匹配问题,也就是我们常说的查找子串问题。

术语解释

  • 主串(目标串): 简单来说就是被搜索的字符串,一般来说就是那个长的。

  • 模式串:被匹配,是查找的目标。

  • 算法目标:字符串中的模式定位问题,简单来说就是查找子串,在主串中查找匹配模式串。这一类的算法,又被我们称为模式匹配算法。

说到模式匹配算法,就绕不开暴力,暴力算法的也是有名字的,叫 BF 算法。

BF 算法

BF 算法是一种纯暴力的字符串匹配算法,比较符合人类自然思维方式的方法,即对源字符串和目标字符串逐个字符地进行比较。算法过程,不断的比对,直到在主串中找到目标串或者,遍历完目标串。

例如:S="ABABAC" T="ABA"

匹配过程:

A B A B A C
A B A
匹配失败,继续匹配
A B A B A C
  A B A
匹配失败,继续匹配
A B A B A C
    A B A
匹配成功,结束

我们发现 BF 算法每次 t 匹配失败都往后移动一个位置,有没有一种可能,在一次匹配中我们能判断移动几次,或者说我们能不能知道向后移动几个是一定不能找到正确答案,然后进行剪枝。

沿着这个思路我们开始考虑一下 KMP 算法。

KMP 算法详解

如果按照正常 BF 算法做字符串匹配的话,我们能够写出如下代码

T = "ABABAC"
P = "ABA"

def BF(tString, pString):
    i = 0 #主串中的位置
    j = 0 # 模式串中的位置
    for i in range(len(tString)-len(pString)+1):
        isPatt = True
        for j in range(len(pString)):
            if tString[i+j]!=pString[j]:
                isPatt = False
                break
        if isPatt:
            return i
    return -1

print(BF(T, P))

我们看到 i 每次匹配不成功,就回到了初始位置,再向后移动一位。

对于正确性,上面的算法确实是毫无疑问,没有任何问题。但是对于效率上面的算法,确实非常低劣。

而我们将会使用 KMP 算法解决上面这种低效回退的问题。

KMP 算法的核心思想是,利用已经匹配过的这一部分有效的信息,保持主串位置 i 的值不变,去修改模式串的位置 j 的值。

而 KMP 算法就是告诉我们这个 j 该如何去改变。

在讲 Next 数组之前,我们先来讨论一下前后缀的概念。

字符串前后缀:

到这里听不懂的建议直接去背模板,我们以拿奖为最终目标,以了解并且会使用为最终目标即可。

前缀:

符号串左部的任意子串(或者说是字符串的任意首部),在 KMP 算法中使用的是“真”前缀,即不包含自己前缀。

简单记忆方式: 前缀要找除了自己,且从头开始的所有子串。

后缀:

符号串右部的任意子串(或者说是字符串的任意尾部),在 KMP 算法中使用的是“真”后缀,即不包含自己后缀。

简单记忆方式: 后缀要找除了自己,且以最后一个字符结尾的所有子串。

举例:

求 Next 数组前,我们还要去了解一下最长公共前后缀,他的长度对于 KMP 的 Next 数组的计算有着紧密的联系。

最长公共前后缀:

后面的文中将真省略,大家注意。

如果设模式串 P = "abaabca" ,那我们可以得到以某个位置结尾的子串的最长公共前后缀长度。

 

我们可以得出以下表格:

对于 Next 数组将整体向右移动一位后,在左侧补-1。

 

Next 数组的含义:

对于模式串"abaabca"而言

出现这种从第 1 位就匹配出错的情况,即使通过人工我们不能找到任何优化方式,于是只能将模式串右移。

 

恰好模式串的 -1 的位置正好置于主串的 i 位置,这就是 next 的第一位补-1 的原因。

我们再看第二种情况,部分匹配成功的情况。

仍对于模式串"abaabca"而言:

设主串为"abaaefaged",那么会有:

在主串的 i=4 位置,模式串 j=4 的位置发生了不匹配。

我们通过最优人工移动可以得到

恰好与主串 i 位置对应的值是模式串的 1 号位置。

那么与我们计算出的 Next 的数组值相同。

重点来了,我们说一下为什么可以这么神奇!!!

原因:

由于我们通过 next 数组计算,而 next 数组来源于最长公共前后缀的长度,那么为什么最长公共前后缀就能计算出,转移目标呢?

假设某个字符串 s 的最长共前后缀为 X="abcd...",那么这个字符串一定是一下结构,开头是 X 结尾是 X 中间可能会有重叠,匹配到 s 的最后一个字符失败后,那我们知道 X 肯定是匹配成功了,因为 X 不包含最后一个字符。

既然我们知道 X 匹配成功,那么我们一定知道,在主串中一定是从 i 位置开始且一定有一个 X 与模式串中的 X 匹配成功。

如下,点为省略号:

而我们又已知,字符串 s 一定有一个后缀 X,那么我们直接用 s 的后缀 X 去匹配主串的 X,且 X 是最长公共前后缀,那么我们就完成了最优的转移。

当 s 是模式串的从头开始的子串时,就可以得到从某一个字符不匹配时的转移情况。

基本原理已经讲清楚了,我们开始说 KMP 算法。

KMP 算法框架和 BF 大致相似,根据上面的分析,对于字符串的比对我们分为三种。

  1. T[i]==P[j] 的情况

    此时,两字符相同应该继续比对所以:

    i=i+1

    j=j+1

  2. T[i]!=P[j] 的情况

    此时,两字符不相同,j 应该根据 next 数组进行转移,所以:

    j=next[j]

  3. j=-1 的情况

    因为,j=next[j],且 next 第一位为 -1 即出现了第一位就匹配失败的情况,那我们应该做的是,是的模式串的开头向后移动,即:

    j=j+1

    那么 j 对应 i 的位置也变成了 i+1 所以:

    i=i+1

    那么与情况 1 相同,我们一同考虑。

**!!!!!!**至此,KMP 算法整体思路完成。

我们可以得到一下 KMP 的模板:

 

def KMP(sStart, s, p):
    # tStart 从主串的哪个位置开始,从头开始为0
    # s 主串
    # p 模式串
    i = sStart  # 主串中的位置
    j = 0  # 模式串中的位置
    sLen = len(s)
    pLen = len(p)

    while i < sLen and j < pLen:
        if j==-1 or s[i] == p[j]:
            # 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
            i += 1
            j += 1
        else:
            j = next[j]
            # 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = nextt[j]
            #    next[j]即为j所对应的next值
    if j == pLen:
        return i - j
    else:
        return -1

 KMP 算法讲解至此结束,但是我们漏下了一个东西,那就是 Next 数组的计算

def GetNext(p):
    pLen = len(p)
    next[0] = -1
    k = -1
    j = 0

    while j < pLen - 1:
        # p[k]表示前缀,p[j]表示后缀
        if k == -1 or p[j] == p[k]:
            j += 1
            k += 1
            next[j] = k
        else:
            k = next[k]

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

恁说叫啥就叫啥

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

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

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

打赏作者

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

抵扣说明:

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

余额充值