彻底搞懂KMP,一篇就够了

前言:看毛片算法漫画讲解超幽默有爱~

一,KMP算法解决什么类型的问题

String str1 = "bacbababadababacambabacaddababacasdsd";
String str2 = "ababaca";

由以上字符串我们知道,str1有两处包含str2
分别在str1的下标为10,26的位置

“bacbababad**ababaca**mbabacadd**ababaca**sdsd”;

二,算法讲解与说明

一般匹配字符串时,我们从目标字符串str1(假设长度为n)的第一个下标选取和str2长度(长度为m)一样的子字符串进行比较,直到str1的末尾(实际比较时,下标移动到n-m)。这样子的复杂度就是O(n*m)。而KMP算法的复杂度可以优化为O(n+m)

简化时间复杂度的原因:充分利用了目标字符串str2的性质(比如里面部分的字符串的重复性,即使不存在重复字段,在比较时实现最大的移动量。

三,考察目标字符串str2

ababaca
这里我们先要计算一个长度为m的转移函数next。next数组的含义就是一个固定字符串的最长前缀和最长后缀相同的长度。
比如:abcjkdabc,那么这个数组的最长前缀和最长后缀相同的话必然是abc。
cbcbc,这个数组的最长前缀以及最长后缀相同就是cbc。
abcbc,最长前缀以及最长后缀相同就是不存在的。

注意:最长前缀是说以第一个字符开始,但是不包含最后一个字符。
比如我们说aaaa的最长前缀以及后缀都是aaa。

对于目标字符串str2,ababaca,长度为7,next[0],next[1],next[2],next[3],next[4],next[5],next[6]分别计算的是a,aab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀的长度。
由于a,ab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀是“”,“”,“a”,“ab”,“aba”,“”,“a”, 所以next数组的值是[-1,-1,0,1,2,-1,0]。这里-1表示不存在,0表示存在长度为1,2表示长度为3.这是为了和代码相对应。

下图中的1,2,3,4是一样的。1-2之间的和3-4之间的也是一样的,我们发现A和B不一样;之前的算法是我把下面的字符串向前移动了一个距离。重新重头开始比较,那必然存在很多重复的比较。现在的做法就是把下面的字符串往前移动,使得3和2对其,直接比较C和A是否一样。

四,代码解析继续

假设现在文本串是匹配到了i位置,模式串匹配到了j位置
(1)如果j = -1, 或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
继续匹配下一个字符。
(2)如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则i不变,
而j = next[j] .此举意味着失配时,模式串P相对于文本串S向右移动了
j - next[j] 位。
换句话说就是当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next数组,next数组 的求解后续会重点讲解,即也就是移动的实际位数为:j - next[j]。且此值大于等于1。

(3)很快你就会发现next数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next[j] = k,代表j指针之前的匹配上的字符串中有最大长度为k的相同前缀后缀。

(4)其实这也意味着在某个字符串失配时,该字符对应的next值会告诉你下一步匹配中。模式串应该跳到哪个位置(跳到next[j]位置)。如果next[j] 等于0
或者-1,则跳到模式串的开头字符,若next[j] = k或者k>0,代表下次匹配跳到j之前的某个字符,而不是跳到开头,且具体跳过了K个字符。

四,KMP算法代码里面最关键代码参数说明
1. next[j] = k

    /**
     * 获取执行KMP算法需要的next数组(采用递归的方式获取)
     */
    public static void getnext(String ptr,int[] next){

        char[] p = ptr.toCharArray();
        int plen = ptr.length();
        next[0] = -1;
        int k = -1;
        int j = 0;
        while(j < plen - 1){
            // p[k]表示前缀,p[j]表示后缀
            if(k == -1 || p[j] == p[k]){
                ++ k;
                ++ j;
                if(p[j] != p[k]){
                    next[j] = k;
                }else {
                    // 因为不能出现p[j] == p[next[j]],所以出现之后继续递归,k = next[k] =next[next[k]]
                    next[j] = next[k];
                }
            }else{
                k = next[k];
            }
        }
    }

在这里插入图片描述
其中最关键的就是next[j] = k,就是由以上说明得来的。

2, k = next[k];
在这里插入图片描述这个式子得来的关键就是使用模式串的自我匹配方式寻找到公共的前后缀。
// 因为不能出现p[j] == p[next[j]],所以出现之后继续递归,k = next[k] =next[next[k]]
next[j] = next[k];
这个就是防止重复出现上一次的失配情况。

五,最后是KMP算法解决字符串匹配位置问题的源代码

/**
 *  使用KMP算法实现在str字符串中查找ptr字符串称为模式字符串
 */
import java.util.Scanner;
public class KMP {

    public static void main(String[] args){

        Scanner cin = new Scanner(System.in);

        String str = "abcderde";

        String ptr = "bcd";

        int[] next = new int[ptr.length()];

        getnext(ptr,next);

        int ans  = kmpsearch(str,ptr,next);

        System.out.println(ans);;

    }
    /**
     * 获取字符串开始匹配的位置
     */
    public static int kmpsearch(String str,String ptr,int[] next){

        char[] s = str.toCharArray();
        char[] p = ptr.toCharArray();
        int i = 0;
        int j = 0;
       int slen = str.length();
       int plen = ptr.length();
       while(i < slen && j < plen){
           //  如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),我们都是令i++,j++.
           if(j == -1 || s[i] == p[j]){
               i++;
               j++;
           }else{
                j = next[j];
           }
       }
       if(j == plen){
            return i - j;
       }else{
           return -1;
        }
    }

    /**
     * 获取执行KMP算法需要的next数组(采用递归的方式获取)
     */
    public static void getnext(String ptr,int[] next){

        char[] p = ptr.toCharArray();
        int plen = ptr.length();
        next[0] = -1;
        int k = -1;
        int j = 0;
        while(j < plen - 1){
            // p[k]表示前缀,p[j]表示后缀
            if(k == -1 || p[j] == p[k]){
                ++ k;
                ++ j;
                if(p[j] != p[k]){
                    next[j] = k;
                }else {
                    // 因为不能出现p[j] == p[next[j]],所以出现之后继续递归,k = next[k] =next[next[k]]
                    next[j] = next[k];
                }
            }else{
                k = next[k];
            }
        }
    }
}

结尾:给大家一个大神的链接,超详细的讲解KMP算法,基本上没有任何的理解问题。
非常详细而且还带有源代码解析,感谢博主的分享~

从头到尾彻底理解KMP

在这里插入图片描述

我们这里说的KMP不是拿来放电影的(虽然我很喜欢这个软件),而是一种算法。KMP算法是拿来处理字符串匹配的。换句话说,给你两个字符串,你需要回答,B串是否是A串的子串(A串是否包含B串)。比如,字符串A="I'm matrix67",字符串B="matrix",我们就说B是A的子串。你可以委婉地问你的MM:“假如你要向你喜欢的人表白的话,我的名字是你的告白语中的子串吗?” 解决这类问题,通常我们的方法是枚举从A串的什么位置起开始与B匹配,然后验证是否匹配。假如A串长度为n,B串长度为m,那么这种方法的复杂度是O (mn)的。虽然很多时候复杂度达不到mn(验证时只看头一两个字母就发现不匹配了),但我们有许多“最坏情况”,比如,A= "aaaaaaaaaaaaaaaaaaaaaaaaaab",B="aaaaaaaab"。我们将介绍的是一种最坏情况下O(n)的算法(这里假设 m<=n),即传说中的KMP算法。 之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。这时,或许你突然明白了AVL 树为什么叫AVL,或者Bellman-Ford为什么中间是一杠不是一个点。有时一个东西有七八个人研究过,那怎么命名呢?通常这个东西干脆就不用人名字命名了,免得发生争议,比如“3x+1问题”。扯远了。 个人认为KMP是最没有必要讲的东西,因为这个东西网上能找到很多资料。但网上的讲法基本上都涉及到“移动(shift)”、“Next函数”等概念,这非常容易产生误解(至少一年半前我看这些资料学习KMP时就没搞清楚)。在这里,我换一种方法来解释KMP算法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Tronhon

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

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

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

打赏作者

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

抵扣说明:

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

余额充值