KMP算法

本篇文章引用自:很详尽KMP算法(厉害)

定义:

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

暴力匹配:

假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?

如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:

  • 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
  • 如果失配(即S[i] != P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。

举个例子,如果给定文本串S“BBC ABCDAB ABCDABCDABDE”,和模式串P“ABCDABD”,现在要拿模式串P去跟文本串S匹配。由下图可以看到,当模式串匹配到字符D时,发生了失配,此时i = 10,j = 6:
在这里插入图片描述
按照暴力匹配的思路,我们现在应该将i设置为10-6+1=5,j设置为0,即使得模式串从头与文本串的下一个字符进行匹配:
在这里插入图片描述
但是很显然,无论是字符B还是C、D,与模式串首字符A匹配都会失配,那么有没有一种方法可以告知我们应该从何处开始匹配,从而避开这些必然会失配的字符呢?KMP算法就是这样一种方法。

KMP算法:
介绍KMP之前,我们需要先了解什么是最大前缀后缀公共元素长度。

  • 对于P = p0 p1 …pj-1 pj,寻找模式串P中长度最大且相等的前缀和后缀。如果存在p0 p1 …pk-1 pk = pj- k pj-k+1…pj-1 pj,那么在包含pj的模式串中有最大长度为k+1的相同前缀后缀。举个例子,如果给定的模式串为“abab”,那么它的各个子串的前缀后缀的公共元素的最大长度如下表格所示:

在这里插入图片描述
以下部分都以maxLen表示最大前缀后缀公共元素长度。

  • 子串a无前缀后缀,所以其maxLen为0
  • 子串ab的前缀为a,后缀为b,无公共元素,所以其maxLen为0
  • 子串aba的前缀为a、ab,后缀为a、ba,公共元素为a,所以其maxLen为1
  • 子串abab的前缀为a、ab、aba,后缀为b、ab、bab,公共元素为ab,所以其maxLen为2

由此,我们可以得到模式串“abab”的最大前缀后缀公共元素长度数组maxLen[ ],后续该数组在KMP算法中将担任重要的角色。
按照下图的方法,可以快速看出各个子串的maxLen:
其中maxLen[0]永远为0。

在这里插入图片描述
下面进入正题,先直接给出KMP的算法流程
假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置:

  • 如果当前字符匹配成功,即S[i] == P[j],令i++,j++,继续匹配下一个字符
  • 如果当前字符匹配失败,即S[i] != P[j],则令 i 不变,j = maxLen[j-1](若j==0,直接i++即可),继续将S[i]与P[j]进行匹配。此举意味着失配时,模式串P相对于文本串S向右移动了j - maxLen[j-1] 位。换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符之前的子串对应的maxLen值。

为什么要使用maxLen数组以及 j 为什么要移动到maxLen[j-1]?

下面这张图很好地解释了这个问题:当模式串的后缀pj-k pj-k+1,…, pj-1 跟文本串si-k si-k+1,…,si-1匹配成功,但pj 跟si匹配失败时,因为maxLen[j-1] = k,说明在不包含pj的模式串中有最大长度为k 的相同前缀后缀,即p0 p1 … pk-1 = pj-k pj-k+1 … pj-1,故令j = maxLen[j-1],从而让模式串右移 j - maxLen[j-1] 位,使得模式串的前缀p0 p1,…,pk-1对应着文本串 si-k si-k+1,…,si-1,而后让pk 跟si 继续匹配。

在这里插入图片描述
因此,maxLen数组的作用在于提供失配字符之前子串的最大前缀后缀公共元素的长度k,
由于前缀公共元素与后缀公共元素完全相同,所以后缀公共元素所匹配的文本串,前缀公共元素也绝对匹配,我们只需将模式串的前缀公共元素的下一位(下标为k)与原来文本串的失配位进行比较即可。

那么如何通过代码计算出一个模式串的maxLen数组?

再使用上面的一张图:

在这里插入图片描述
设i为字符数组p的下标,len为字符i之前子串的最长前缀后缀公共元素的长度,因为maxLen[0]永远为0,所以我们从i=1开始求子串的maxLen:

  • 若p[i] = p[len],则maxLen[i] = len++;因为在添加字符i之前,字符串已经有长度为len的前缀后缀公共元素,若是添加的字符i与前缀公共元素的后一个字符相同,那么添加字符i后的字符串的maxLen就等于len++。
  • 若p[i] != p[len],则令len=maxLen[len-1],然后继续比较p[i]与p[len],若p[i] != p[len]且len=0,则令maxLen[i] = 0,i++;

下面粗略地解释一下 为什么要令len=maxLen[len-1]:
当p[i] != p[len]时,可把maxLen的求解看成是一个模式匹配的问题,整个模式串既是主串又是模式串,如下图所示:

在这里插入图片描述
A(下标为i)与B(下标为len)失配,根据上述文本串匹配规则,我们需要令模式串的下标len等于失配字符之前字符串的maxLen,然后再将p[len]与p[i]进行匹配。在这个例子中,maxLen=maxLen[len-1]=1,所以令len=maxLen[len-1]=1后再次匹配:

在这里插入图片描述
此时A与B仍然失配,继续执行上一步操作,令len=maxLen[len-1]=0,再将p[len]与p[i]进行匹配:

在这里插入图片描述
此时匹配成功,则maxLen[i] = len++,i++,继续计算下一个子字符串的maxLen。

JAVA 代码实现:

package DataStructure;


public class KMPDemo {
    public static void main(String[] args) {
        String dest = "KAVFBAPLKABAHJKFABABKKKF";
        String patten = "ABA";
        int index = kmp(dest.toCharArray(), patten.toCharArray());
        System.out.println(index); // 9
    }

    public static int[] getMaxLen(char[] patten) {
        int[] maxLen = new int[patten.length];
        maxLen[0] = 0;
        int i = 1; // 从i=1开始遍历
        int len = 0;
        while (i < patten.length) {
            while (len > 0 && patten[i] != patten[len]) {
                len = maxLen[len - 1];
            }
            if (patten[i] == patten[len]) {
                maxLen[i++] = ++len;
            }else {
                // patten[i] != patten[len] 且 len=0时:
                maxLen[i++] = 0;
            }
        }
        return maxLen;
    }

    public static int kmp(char[] dest, char[] patten) {
        int[] maxLen = getMaxLen(patten);
        int i = 0;
        int j = 0;
        while (i < dest.length && j < patten.length) {
            if (dest[i] == patten[j]) {
                i++;
                j++;
            }else {
                if (j > 0) {
                    j = maxLen[j - 1];
                }else {
                    i++;
                }
            }
        }
        // j == patten.length说明找到了
        if (j == patten.length) {
            return i - j;
        }
        return -1;
    }
}

Python 代码实现:

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        if not needle: return 0
        def getNext(s):
            n = len(s)
            next_list = [0] * n
            # l 表示上一个子串的最长公共前后缀
            l = 0
            for i in range(1, n):
                if s[i] == s[l]:
                    next_list[i] = l + 1
                    l += 1
                else:
                    while l > 0 and s[i] != s[l]:
                        l = next_list[l-1]
                    if s[i] == s[l]:
                        next_list[i] = l + 1
                        l += 1
                    else:
                        next_list[i] = 0
        
            return next_list

        def kmp(haystack, needle):
            next_list = getNext(needle)
            m, n = len(haystack), len(needle)
            i, j = 0, 0
            while i < m and j < n:
                if haystack[i] == needle[j]:
                    i += 1
                    j += 1
                else:
                    if j > 0:
                        j = next_list[j-1]
                    else:
                        i += 1
            if j == n:
                return i - j
            else:
                return -1

        return kmp(haystack, needle)



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值