KMP算法

243 阅读9分钟

一个人能走的多远不在于他在顺境时能走的多快,而在于他在逆境时多久能找到曾经的自己! ----KMP

(233333

参考视频

最浅显易懂的 KMP 算法讲解(动画配图)

油管阿三哥讲KMP查找算法

帮你把KMP算法学个通透!(卡尔)

什么是KMP

说到KMP,先说一下KMP这个名字是怎么来的,为什么叫做KMP呢。

因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP

kmp人物.png

KMP有什么用

KMP主要应用在字符串匹配上。

如果使用暴力匹配方法,算法的时间复杂度明显为O(m*n),其中m,n为主串和子串的长度。

kmp字符串匹配(暴力).png KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。

kmp跳过.png

所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。

什么是前缀表

这个next数组究竟是个啥呢?

next数组就是一个前缀表(prefix table)。

前缀表有什么作用呢?

前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

为了清楚地了解前缀表的来历,我们来举一个例子:

要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

如动画所示:

KMP精讲1.gif

把子串aa 标记上了,这是有原因的,大家先注意一下,后面还会说到。

可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。

但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。

此时就要问了前缀表是如何记录的呢?

首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。

那么什么是前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

最长相等前后缀

文章中字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串

后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

正确理解什么是前缀什么是后缀很重要!

为什么一定要用前缀表

这就是前缀表,那为啥就能告诉我们 上次匹配的位置,并跳过去呢?

回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图:

KMP01.png 然后就找到了下标2,指向b,继续匹配:如图

KMP02.png

以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要!

下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。

所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。

很多介绍KMP的文章或者视频并没有把为什么要用前缀表?这个问题说清楚,而是直接默认使用前缀表。

如何计算前缀表

接下来就要说一说怎么计算前缀表。

如图:

KMP001.png

长度为前1个字符的子串a,最长相同前后缀的长度为0。(注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。)

KMP002.png

长度为前2个字符的子串aa,最长相同前后缀的长度为1。

KMP003.png

长度为前3个字符的子串aab,最长相同前后缀的长度为0。(前缀a、aa,后缀b、ab)

以此类推: 长度为前4个字符的子串aaba,最长相同前后缀的长度为1。 (前缀a、aa、aab,后缀a、ba、aba)

长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。(前缀a、aa、aab、aaba,后缀a、aa、baa、abaa)

长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。

那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:

KMP004.png

可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:

KMP精讲2.gif

找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。

为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。

所以要看前一位的 前缀表的数值。

前一个字符的前缀表的数值是2, 所以把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。

最后就在文本串中找到了和模式串匹配的子串了。

前缀表与next数组

很多KMP算法的实现都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?

next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。

为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。

其实这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。

这里我们用next数组作为前缀表(不减一)。

结合题目

力扣26. 实现 strStr()

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

示例 1:

输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。

示例 2:

输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1

python代码

class Solution:
    def getNext(self, next: List[int], s: str) -> None:
        j = 0
        next[0] = 0
        for i in range(1, len(s)):
            while j > 0 and s[i] != s[j]:# 如果s[i]和s[j]不相等,则j回退到next[j-1]的位置
                j = next[j - 1]
            if s[i] == s[j]:			# 如果s[i]和s[j]相等,则j加1
                j += 1
            next[i] = j					# 将j的值赋给next[i]
    
    def strStr(self, haystack: str, needle: str) -> int:
        if len(needle) == 0:
            return 0
        next = [0] * len(needle)
        self.getNext(next, needle)
        j = 0
        #i指向haystack,j指向needle
        for i in range(len(haystack)):
            while j > 0 and haystack[i] != needle[j]:	
                # 如果haystack[i]和needle[j]不相等,则j回退到next[j-1]的位置
                j = next[j - 1]
            if haystack[i] == needle[j]:	# 如果haystack[i]和needle[j]相等,则j加1
                j += 1
            if j == len(needle): 	# 如果j等于needle的长度,则返回i-needle的长度+1
                return i - len(needle) + 1
        return -1

以haystack = "aabaabaaf" , needle = "aabaaf" 为例

getNext()中i和j的含义

  1. i(Index)
    • i 是一个索引变量,用于遍历模式字符串(在这个例子中是 aabaaf)。
    • 它的值从 1 开始,一直到模式字符串的长度减 1(因为第一个字符的 next 值已经初始化为 0)。
    • i 指向当前正在考虑的模式字符串中的字符。
  2. j(Match Index)
    • j 是一个用于跟踪当前匹配长度的指针,它指向模式字符串中已经匹配的部分的最右侧字符。
    • 在算法开始时,j 被初始化为 0,表示没有匹配的字符。
    • s[i](当前考虑的字符)与 s[j]j 指向的字符)相等时,j 增加,表示匹配长度增加。
    • s[i]s[j] 不相等时,j 会根据 next 数组回退到一个合适的位置,以便重新开始匹配。

ij 的关系可以这样理解:

  • i 总是指向下一个要检查的字符。
  • j 总是指向当前匹配序列的最右侧字符。

在 KMP 算法中,next 数组的作用是记录在模式字符串中,对于任意位置 i,最长的相同前缀和后缀的长度。这个长度就是 j 的值。当出现不匹配时,模式字符串会根据 next 数组的值进行移动,以跳过已经匹配的部分,从而提高匹配效率。

总结来说,i 是遍历模式字符串的索引,而 j 是当前匹配的前缀长度的指示器。通过这两个指针的协同工作,KMP 算法能够有效地处理模式匹配问题。

strStr()中i和j的含义

i是 haystack 的索引,j是 needle 的索引。

haystack[i]needle[j]相等时,j加1;否则,j更新为next[j-1],直到haystack[i]needle[j]相等或j为0。

j等于模式串的长度时,说明匹配成功,返回主串中匹配的起始位置。

遍历完整过程

KMP遍历过程.jpg