KMP算法理解(串的模式匹配算法)


前言

子串的定位通常称为模式匹配,题目的要求是:在主串S中找到第一次出现完整子串T(T被称为模式串)时的起始位置,没找到就返回-1。这也是数据结构中“串”的常见算法

示例如下所示:

输入:S = "hello", T = "ll"
输出:2

提示:以下是本篇文章正文内容,下面案例可供参考

一、暴力解

首先拿到这题,第一眼想到的就是,暴力遍历呗。
1、初始化:设置指向主串S的指针i和指向模式串的指针j,从左到右进行匹配。
在这里插入图片描述
2、如果遇见字符不对应,那咋办呢?把指针回到上一次比较状态之后的那个位置(图中的B点)。
在这里插入图片描述
在这里插入图片描述
3、不断重复上述步骤,找到了直接返回下标,没有找到就返回-1。

不多说,上代码。

    public int strStr(String haystack, String needle) {
        if(needle.isEmpty()) return 0;
        char[] S = haystack.toCharArray();
        char[] T = needle.toCharArray();

        int i = 0; //index of S
        int j = 0; //index of T
        while(i < S.length & j < T.length){
            if(S[i]==T[j]){
                i++;
                j++;
            }else{
                i = i-j+1;
                j=0;
            }
        }
        if(j==T.length){
            return i-j;
        }else{
            return -1;
        }
    }

这种思路简单,易于理解,在某些场合效率也比较高。容易得到,该算法最好情况下复杂度为O(n+m),n和m分别是主串和模式串的长度。最差情况每找一次都要回溯一次,相当于复杂度为O(n*m)。大神发现了这种算法需要不断地回溯,而且没有充分利用已有的匹配信息,于是经典算法诞生了。

二、KMP算法

1、算法思想

D.E.Knuth、J.H.Morris和V.R.Pratt同时发现,利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置,这样是不是可以提高效率呢?
怎么理解KMP算法的思想呢?

1、只要对S串遍历一遍。(暴力解法中,我们总是需要回溯指向S的指针,来来回回进行匹配)

2、既然只想对S串遍历一遍,如果出现字符不匹配,下一步T串从哪一位开始匹配呢?这里引入next数组进行处理。

先用图来解释next数组的含义。
①指针i和j找到不匹配的字符啦,要是按照暴力法,指针i回到B,指针j回到A,重新开始匹配(如上述算法)。但是KMP不是这么做的。
在这里插入图片描述
②既然i和j对应的元素不匹配,我们想让i不动(不回溯),那么就应该在子串中找一个位置重新开始匹配,我们要把j移到哪呢?目视判别应该是这样移动的。
在这里插入图片描述
为什么要将j移动到指向B点位置呢?因为主串中i前面的是A,模式串B前面的也是A。是的,由眼睛我们很容易发现这个性质,数学公式怎么表达呢?
当匹配失败时,j要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的。这个不难理解,再画个图吧。
在这里插入图片描述
如图中,在T串中,前面三个字符ABG与j指针前面的三个字符是一致的。所以因为S[i]=Z不等于T[j]=H,所以j指针要移动,移动到哪呢?需要移动到H处。为啥呢?,因为ABG这三个字符已经匹配好了嘛。

总结一下:

1、我们需要做的事情是,需要确定在字符串T中第j个字符不匹配时,指针j需要移动到哪的问题,程序中用next[j]记录下一个移动的位置。
2、next数组与S串无关,只是T串的固有属性。
3、在匹配过程中,S串指针i没有进行回溯。
用数学公式来表示就是:T[0,K-1]==T[j-k,j-1]

补充说明

该规律是KMP算法的关键,KMP算法是利用待匹配的子串自身的这种性质,来提高匹配速度。该性质在许多其他中版本的解释中还可以描述成:若子串的前缀集和后缀集中,重复的最长子串的长度为k,则下次匹配子串的j可以移动到第k位(下标为0为第0位)。我们将这个解释定义成最大重复子串解释。

这里面的前缀集表示除去最后一个字符后的前面的所有子串集合,同理后缀集指的的是除去第一个字符后的后面的子串组成的集合。举例说明如下:

在“aba”中,前缀集就是除掉最后一个字符’a’后的子串集合{a,ab},同理后缀集为除掉最前一个字符a后的子串集合{a,ba},那么两者最长的重复子串就是a,k=1;

在“ababa”中,前缀集是{a,ab,aba,abab},后缀集是{a,ba,aba,baba},二者最长重复子串是aba,k=3;

在“abcabcdabc”中,前缀集是{a,ab,abc,abca,abcab,abcabc,abcabcd,abcabcda,abcabcdab},后缀集是{c,bc,abc,dabc,cdabc,bcdabc,abcdabc,cabcdabc,bcabcdabc},二者最长重复的子串是“abc”,k=3;

2、next数组求法

next数组是用来记录第j个字符不匹配时,指针j需要跳转的位置。集next[j]=k代表T[i] != P[j]时,j指针的下一个位置。next数组是KMP算法的灵魂。

然后呢?具体怎么求next数组呢?找规律。
1、j=0时,情况如图所示,只能往0跳,左边没位置了。
在这里插入图片描述
2、j=1时,情况如图所示,也只能往0跳了吧,没位置了。
在这里插入图片描述
3、出现T[k] == T[j]时

在这里插入图片描述
存在这样的特性:

next[j+1] == next[j] + 1

怎么理解呢?T[k] == T[j]就代表重复的字符多了一个嘛!那k明显就是在原来的基础上+1。

  • 贴个证明过程哈
    因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)
    这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。
    即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。

3、T[k] != T[j]时
在这里插入图片描述
代码是这样的:

k=next[k]

看完代码,我人傻了。跳的太快了,怎么理解呢?看了几篇blog,之后我有点理解了。

首先,明确k的含义是当前j需要跳转到的下一个位置。如果T[k]!=T[j],那显然k值就要变化了,不能是T[k]==T[j]时那种+1了。
在这里插入图片描述

正如上图中所示,C!=B,此时next[j]=k的,我们要确定的是next[j+1]的位置是在哪?T[j+1]=C,现在是要求next[j+1]。意思就是要求前面黄色部分中ABA挑一个位置knew来与T[J]=B进行字符比较,这个knew在哪呢?因为黄色部分ABA与蓝色部分ABA显然是已经匹配好的,此时T[k]与T[j]不同,要找下一个匹配位置,不正是next[k]存好了吗?,即存在knew=next[k]。
在这里插入图片描述
你可能会有疑问:

1、为啥T[k]!=T[j]时,只需要考虑从T[0,k-1]找一个新的位置knew,为啥knew不能出现在其他位置,比如大于k。

  • 因为k之前已经匹配好了,k位置不匹配了,后面不能出现更大的k,毕竟k位置就不满足要求了。

2、为啥k = next[k]?不是很理解。

  • 第一点,始终记得next[j]的值(也就是k)表示,当P[j] != T[i]时,j指针的下一步移动位置。
  • 第二点,转换思路。T[0,k-1]与T[j-k,j-1]这一段匹配好了,但是T[k]!=T[j],我们现在是要更新得到knew,使得T[0,knew]与T[j-knew,j-1]匹配好,此时再比较T[knew]与T[j]。(注意此时j值没变哈,只更新k)。

贴个代码,如果不是很清楚,跟着代码跑几遍,就有自己的理解了。

class Solution {
    public int strStr(String haystack, String needle) {
        char[] t = haystack.toCharArray();
        char[] p = needle.toCharArray();
        if(p.length==0){
            return 0;
        }
        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        int[] next = getNext(needle);
        while (i < t.length && j < p.length) {
            if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0
                i++;
                j++;
            } else {
                j = next[j]; // j回到指定位置
            }
        }
        if (j == p.length) {
            return i - j;
        } else {
            return -1;
        }
    }

    public static int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
           if (k == -1 || p[j] == p[k]) {
               next[++j] = ++k;
           } else {
               k = next[k];
           }
        }
        return next;
    }
}

总结

KMP算法好难呀,很多文章、视频感觉都没讲清楚,虽然我觉得我也没讲清楚,但是算是自己的一种理解吧,手撕这段代码有点难。KMP算法精妙在于其很好的利用了之前的匹配信息,其实这个算法效率也并没有提升太多。难以理解主要是思路,还有那段经典代码。
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值