其他分享
首页 > 其他分享> > KMP

KMP

作者:互联网

KMP算法

目录

问题

例如:在文本串S(长度为N)中查找模式串P(长度为M)出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

img

Brute Force

一种暴力做法就是从txt的首字符开始配对txt和pat,配对不成功就从txt的下一个字符开始再做一次

时间复杂度:O(MN)

空间复杂度:O(1)

KMP

简介

Knuth-Morris-Pratt 字符串查找算法,简称为 KMP算法,常用于在一个文本串 S 内查找一个模式串 P 的出现位置。这个算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法。

img

算法逻辑

假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置

换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值(next 数组的求解会在下文详细阐述),即移动的实际位数为:j - next [j],且此值≥1。

BF匹配失败,就把模式串P右移一位,而KMP匹配失败后,模式串P会移动 j - next [j] 位,缩短了时间是空间换时间的做法。

什么是next数组

原理:

取出文本串 S 和模式串 P 匹配成功的部分,计算这部分前后缀的公共元素,实际就是用模式串 P 的前缀去匹配文本串 S 的后缀。

列举出模式串P匹配文本串S过程中可能出现的几种情况以及对应匹配好的部分

序号 匹配情况
1 第一位失配
2 第二位失配 a
3 第三位失配 a b
4 第四位失配 a b a
5 第五位失配 a b a a
6 第六位失配 a b a a b
7 第七位失配 a b a a b c
8 第八位失配 a b a a b c a

定义:

​ 前缀为除了最后一个字符以外,一个字符串的全部头部组合;

​ 后缀为除了第一个字符以外,一个字符串的全部尾部组合。

以第6种失配情况为例枚举出前后缀。

a b a a b
前缀
a
a b
a b a
a b a a
后缀
b
a b
a a b
b a a b

最长的前后缀的公共元素为ab,长度为2

对所有匹配情况下做相同的操作,记录每个最长前后缀的公共元素长度。

失配位置索引 0 1 2 3 4 5 6 7
失配字符 a b a a b c a c
前后缀公共元素最大长度 0 0 0 1 1 2 0 1

我们将其称为next数组或者fail数组,记录着字符串匹配过程中,发生字符失配时可以从那个位置开始重新匹配。

怎么求next数组?

下面用图示来说明:

当前匹配位置索引为 I (I=J+1),已知next[J]=K,想求出next[I]

如果P[J] = P[K],则皆大欢喜, next[I]= next[J]+1
如果P[J] !=P[K],只能寻找更短的相同前后缀匹配,我们看下图

因为红色部分是已经匹配好的,两部分红色都可以是灰+红+灰

如果P[J] = P[next[K]],则皆大欢喜, next[I] = next[K]+1 = next[ next[J] ]+1
如果P[J] !=P[next[K]],继续寻找更短的相同前后缀匹配

结论:求解某个位置(Inext值是一个循环过程,不断检查 上一位置(J)与其(J)最长前缀的后一位(K)是否相等。如果相等next[I] = next[J] + 1,否则 K = next[K]。通过递归索引K = next[K],就能找到长度更短的相同前缀后缀

	n = len(S)
        m = len(P)
        # ———— 构建next ————————————————————————————————————————————
        Next =[0,0]
        k = 0
        for i in range(2,m):
            j = i-1
            k = Next[j]
            # 匹配失败,递归k寻找更小的前缀,直到为0
            while k and P[k] != P[j]:
                k = Next[k]
            # 匹配成功
            if P[k] == P[j] :
                k+=1
                Next.append(k)
            else:
                Next.append(0)

next数组的优化

比如,如果用之前的next 数组方法求模式串“abab”的next 数组,可得其next 数组为0 0 0 1,当它跟下图中的文本串去匹配的时候,发现S[3]P[3]失配。

于是去匹配S[3]P[1]

还是失配,事实上,S[3]P[3]失配,拿与P[3]相等的P[1]去继续匹配S[3],结果肯定是失配。

所以写出next数组的时候要检查一遍,如果P[I] =P[next[I]],递归next[I] = next[next[I]]

所以,咱们得修改下求next 数组的代码。

# —————— 优化next——————————————————————————————————————
        for i in range(2,m):
            j = i-1
            l=Next[i]
            while l and P[i]==P[l]:
                l=Next[l]
            if P[i] != P[l]:
                Next[i] = l
            else:
                Next[i] = 0

最终代码

class Solution:
    def strStr(self, S: str, P: str) -> int:
        n = len(S)
        m = len(P)
        # 特别地,模式串为空字符
        if m == 0:
            return 0
        # ———— 构建next ————————————————————————————————————————————
        Next =[0,0]
        k = 0
        for i in range(2,m):
            j = i-1
            k = Next[j]
            # 匹配失败,递归k寻找更小的前缀,直到为0
            while k and P[k] != P[j]:
                k = Next[k]
            # 匹配成功
            if P[k] == P[j] :
                k+=1
                Next.append(k)
            else:
                Next.append(0)
        
        # —————— 优化next——————————————————————————————————————
        for i in range(2,m):
            j = i-1
            l=Next[i]
            while l and P[i]==P[l]:
                l=Next[l]
            if P[i] != P[l]:
                Next[i] = l
            else:
                Next[i] = 0
        # —————— 开始匹配——————————————————————————————————————
        # i:文本串匹配字符索引
        # j:模式串匹配字符索引
        j = 0
        for i in range(n):
            # 匹配不成功,递归j,直到j=0
            while j and S[i] != P[j]:
                j=Next[j]
            if S[i] == P[j]:
                j+=1
            #退出匹配询问
            if j == m:
                return i-j+1
        return -1

参考资料

https://zhuanlan.zhihu.com/p/76348091

https://www.jianshu.com/p/a9b270eb09bb

https://www.cnblogs.com/zhangtianq/p/5839909.html

https://zhuanlan.zhihu.com/p/76348091

标签:字符,匹配,后缀,next,KMP,Next,失配
来源: https://www.cnblogs.com/jiannan-blog/p/13136778.html