KMP算法

参考:KMP 算法详解 - 知乎 (zhihu.com)
【neko】KMP算法【算法编程#7】_哔哩哔哩_bilibili
KMP算法—终于全部弄懂了_June·D的博客-CSDN博客_kmp算法
「天勤公开课」KMP算法易懂版_哔哩哔哩_bilibili

目的

给两个字符串a和b,请找到b在a串中的位置,并返回其第一个字母的索引,若没有,返回-1.
注意为空串时的情况,a为空b不为空,明显是找不到的,应该返回-1,若b为空,应该返回0

先看最原始的暴力解

仔细想想就有思路,我们可以一个个比较,设置ia字符串的索引,j为字符串b的索引,index为匹配位置。那么我们从i,j,index都为0时开始,一个个字符匹配,如果在其中有一个不匹配,就回溯i到++index,j为0,再继续。
理论上这样是可以求解的,但显然时间复杂度是比较大的。
这里给出暴力解的时间复杂度:O(mn)——m,n为a,b串的长度

暴力解法

仙贝们的伟大算法

KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位仙贝共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。

我们用暴力解匹配失败时,i,j都回溯,有些浪费
而kmp算法,利用匹配失败后存储的信息,尽量减少a,b的匹配次数

了解一下字符串的前缀、后缀

前缀集合

后缀集合

好了,现在讲讲KMP算法的回溯策略:我们只回溯j指定位置,不回溯i

KMP例子

这个例子有一些特殊,但我们还是能清楚地看到,当b串最后一个字符匹配失败时,i和j没有回溯,而是把b串往后移
再看一个例子
只看上面的字符串匹配

这是怎么回事呢?怎么回溯呢?

我们来看看木子(一个up主)的例子来说明

可见,B串的前面都和A串匹配,然而倒数第二个字符不匹配

好的,现在我们看看怎么处理,我们不想i和j都回溯那么多,那就应该想或许i或j只回溯到应该回溯的位置。
我们令前面已经匹配的字符串分别为A子串和B子串,此时我们找A子串后缀B子串的前缀的公共部分,我们发现,这时是有的,为aba,那我们就把j回溯这个公共前后缀长度,而i不用回溯,就变成了第三行那样

不过注意,我们是为了演示把B串“挪动”,但实际操作就只是j的变化而已

而找A子串、B子串的交集时,我们可以发现,因为A、B子串是已经匹配的,即相等的,所以我们找A子串后缀和B子串前缀相当于找B子串前、后缀的交集
所以我们可以认为,j指针回溯的位置只与B串有关
综上,j指针回溯的位置,就是B子串最长公共前后缀的长度

next数组

我们分析了j回溯到距离,那么怎么存储呢?
我们设置一个next数组,在A、B匹配之前,就通过B串计算回溯位置,将其存在next中
next[i]存放B[0]到B[i]最长公共前后缀的长度

此时,匹配策略就有了


来,估计只看这个理解不了,来模拟一下吧



画蓝线部分为已经匹配成功的子串,而后面匹配失败,我们找蓝线子串的最长公共前后缀为aba,此时长度为3,那么将j回溯到3(从0(a)开始,1(b),2(a),这些索引是已经匹配的,再加1,从3开始和A匹配),而 i 不动。这样我们看到第三行画黑线部分是可以匹配的,我们只需从黑线之后再匹配就行。



好,继续匹配,发现还是失败,我们再找最长公共前后缀,还是aba,那么 j 回溯

我们发现,回溯之后的第一个就不匹配了,再找aba(此时的子串)的最长公共前后缀,发现为a,j回溯



此时我们发现,有时第一个就不匹配了,再找a(此时的子串)的最长公共前后缀,发现,嗯?没有(就一个元素,算做没有),那长度为0,j回溯为-1(相当于0的前面)了,就是从头开始匹配

这里刚好匹配了,但是注意一点,如果此时还不匹配(比如画蓝线这里a换成其他的字母),这是就要往后移动 i ,使A串与B串匹配,不然只移动 j 的话明显进入死循环了是不是?

构建next数组

我们之前介绍了next数组,还没说怎么构造呢!
我们用B串自己和自己匹配,B[0]到B[i]的前缀和它的后缀匹配

截一个木子的脸哈哈哈

好的,来看一下例子吧
注:这里的next数组从索引为0开始,我们最后会再看看从索引为1开始的

next数组,存的就是B子串相应位置往前的子串的最长公共前后缀长度

我们看到这个例子,i 、 j 索引后面匹配失败了,此时最后一个字符下面怎么填写方便呢?


因为是子匹配,我们可以后移一下,我们发现,这不就很像A、B的匹配吗?

那我们需要填写的长度,不就是画蓝线的子串找最长公共前后缀长度吗?
而这画蓝线的最长公共前后缀长度,我们已经算出来了(就在前面画蓝线的最后一个字符b的下面),刚好就是next[j],值为2
那么 j 就移到2的位置( j = next[ j ] )



我们发现此时是匹配的,那么next最后一个元素为 j + 1,也就是3

时间复杂度分析:O(m + n)


看看索引从1开始的next数组


第一个索引就和A串不配对,就让B串(也称为模式串)1号位(就是第一个字符)就和A串(也称主串)后一个索引比较



如果是2号位与A串不匹配,就让1号位(因为2号位前面的子串的最长公共前后缀长度为0,就移到B串的最前面,即1号位)和A串当前位比较


这里A串第一个字符应该为A,有点小错误


同理,我们看4号位,此时前面的最长公共前后缀长度为1,就让2号位(B串第二个)开始比较

我们可以发现,这些几号位比较,刚好等于最大公共前后缀长度 + 1



我们把不是与当前位比较(而是与后一位比较)的1号位的值改一改

好的,这就是next数组了

代码实现(只有Java)

next数组

    public static int[] Getnext(int next[],String b)
    {
        char[] bStr = b.toCharArray();
        char[] BStr = new char[b.length() + 1];
        BStr[0] = (char)0;
        for(int i = 0;i < bStr.length ; i ++ ){//这里是把字符串也弄成从索引为1开始
            BStr[i+1] = bStr[i];
        }
        next[0] = b.length();//从索引1开始记录
        next[1] = 0;//不管如何,第一位肯定是0
        int j = 0;//j表示的是b串前缀码的索引
        int i = 1;//i不仅是next数组的主索引,还兼职着后缀的索引(很不可思议,建议动手画一画就了解了)
        while (i < next.length - 1 ){
            if(j == 0 || BStr[i] == BStr[j]){//两个条件:1.j为0,此时就是从头开始匹配前后缀的时候;  2.i、j索引相同,也就是前后缀索引相同
                next[++i] = ++j;//此时 i 和 j 都往前推进,且next数组也可以确定一位,就是前一位已经匹配的数目加1
            }else {
                j = next[j];//这里是非常妙的一步,大致意思是如果此位字符不匹配,就让 j 回溯到此字符之前子串的公共前后缀索引处(建议自己画个图理解一下)
            }
        }
        return next;
    }

strStr方法(返回找到的最初下标值)

    public static int strStr (String a , String b) {
        if(b.length() == 0){//b为空字符串,返回0
            return 0;
        }
        if(a.length() == 0){//a为空字符串,若b也为空,返回0,否则找不到,为-1
            if(b.length()==0){
                return 0;
            }else {
                return -1;
            }
        }
        int[] next = new int[b.length()+1];
        next = Getnext(next,b);
        for(int item:next){//打印检查一下,实际使用可以删除
            System.out.println(item);
        }
        char[] aa = a.toCharArray();
        char[] bb = b.toCharArray();
        int i = 1,j = 1;
        while (i<=a.length()&&j<=b.length()){
            if(j==0||aa[i-1]==bb[j-1]){
                i++;
                j++;
            }else {
                j = next[j];
            }
        }
        if(j>b.length()){
            return i-b.length()-1;
        }
        return -1;
    }

你可能感兴趣的:(KMP算法)