KMP算法,讲的很清楚

1. KMP算法是什么,解决了什么问题

KMP算法就是字符串的模式匹配算法,它解决了一个这样的问题:

有一个模式字符串p,设其长度是lenp,以及另一个字符串str,设其长度lenstr,找出p在str中第一次出现的位置,显然要求lenstr>=lenp。

这个问题乍一看不难,问题其实要找str中哪一个子串是p,那就把str中所有长度为lenp的子串拿出来比对一下不就完事了,这就是暴力解法,由于比较简单,我就不上图了

    public static int firstIndexOf(char[] pattern,char[] str){
        if(pattern==null||str==null)return -1;
        int lenP = pattern.length;
        int lenStr = str.length;
        if(lenP==0||lenP>lenStr)return -1;
        int i,j=0,k;
        //j指向任何一个长度为lenp的子串的开始下标,显然的
        //j的范围[0,lenStr-lenP]
        for(;j<=lenStr-lenP;++j){
            i=j;
            k=0;
            //逐个比对,达到模式串的长度就说明找到了,否则i回退到j+1,k归零
            while(k<lenP&&str[i]==pattern[k]){
                i++;
                k++;
            }
            if(k==lenP)return j;
        }
        return -1;
    }

在最坏的情况下,j必须走到lenStr-lenP时才能得到结果,执行基础运算(lenStr-lenP)*lenP次,时间复杂度O(lenStr*lenP)。

2. 暴力破解的改进

这个方法还有什么可以改进的吗?看下图
在这里插入图片描述
答案是肯定的,在讲解如何加速之前,先定义一个概念:前缀

2.1. 前缀的概念

用一张图来展示什么是前缀吧
前缀串
前缀串的性质是显然的,设某个字符串的前缀串长度是lenPreffix,那么这个字符串的前lenPreffix个字符和后lenPreffix个字符是对应相同的
但这个前缀对我们解决这个问题,有什么用呢?
当然有用了,用处大大的,一图胜千言,看下图。
在这里插入图片描述
如果我们有模式串在任何一个字符结尾时的前缀串长度,我们就可以加速匹配的过程。我们用next[i]表示模式串以第i个字符结尾时,其前面的字符串的前缀的长度,如下图所示:
next数字的含意
那求解过程可以如下代码所示:

    public static int firstIndexOf(char[] pattern,char[] str,int[] next){
        if(pattern==null||str==null)return -1;
        int lenP = pattern.length;
        int lenStr = str.length;
        if(lenP==0||lenP>lenStr)return -1;
        int i=0,j=0;
        while(i<lenStr){
            //得到匹配,两个指针移到下一个位置
            if(str[i]==pattern[j]){
                ++i;
                ++j;
            }else if(j==0)
                //如果是第一个就不匹配,只需移动主串指针
                ++i;
            else
                //否则,当前字符不匹配时,j应该调整为pattern[0]...pattern[j-1]前缀串的长度的位置,即next[j]
                j=next[j];
            if(j==lenP)return i-lenP;
        }
        return -1;
    }

2.2. 如何求解next数组

现在,我们只需要求出next数组,问题就迎刃而解了。
那么如何求解next数组呢?我们试图直接求解的时候,大多情况下,都是暴力的,哈哈。暴力解法无非就是从0-j/2一个一个的尝试,时间复杂度可想而知,太麻烦了,比求解原本的问题还麻烦。
正确的解法是,我们可以假设已经求出来了next[j](不要问怎么求来的,就假设😄),设next[j]=k;那就意味着p[0]…p[k-1]和p[j-k]…p[j-1]是对应相同的,此时我们考察p[k]和p[j]的相等关系,如下图所示:
在这里插入图片描述
这样我们就知道怎么求解next数组了,参考代码如下:

    private static int[] generateNext(char[] pattern) {
        int len = pattern.length;
        if (pattern == null || len < 2) return null;
        int[] next = new int[len];
        //用k代表已经求出来的上一个next的值,k最开始代表next[0],对于next[0]特殊赋值为-1
        int k=-1;
        next[0]=k;
        int i=1;
        //从i到len-1逐个求next值
        while(i<len){
            //如果上一次next值是-1,即特殊值,或者pattern[k]==pattern[i-1],就把next[i]赋值为k+1,同时把k自增1,i自增1
            if(k==-1||pattern[k]==pattern[i-1])
                next[i++]=++k;
            else
                //否则 k回退上一步,直到k=-1
                k=next[k];
        }
        return next;
    }

2.3. 求解next数组优化

其实,当我们求的next[i]是k+1时,如果p[i]==p[next[i]],那么当p[i]没有得到匹配的时候,把i调整为next[i]是没用的,此时可以加一个判断优化一下;

    private static int[] generateNext(char[] pattern) {
        int len = pattern.length;
        if (pattern == null || len < 2) return null;
        int[] next = new int[len];
        //用k代表已经求出来的上一个next的值,k最开始代表next[0],对于next[0]特殊赋值为-1
        int k=-1;
        next[0]=k;
        int i=1;
        //从i到len-1逐个求next值
        while(i<len){
            //如果上一次next值是-1,即特殊值,或者pattern[k]==pattern[i-1],就把next[i]赋值为k+1,同时把k自增1,i自增1
            if(k==-1||pattern[k]==pattern[i-1]){
                int kt = ++k;
                //如果kt!=-1&&pattern[i]==pattern[kt] 就直接回退即可
                while (kt!=-1&&pattern[i]==pattern[kt])kt=next[kt];
                if(kt==-1)kt=0;
                next[i++]=kt;
            } else
                //否则 k回退上一步,直到k=-1
                k=next[k];
        }
        return next;
    }

其实求解这个next数组的过程复杂度有点超过 O ( N ) O(N) O(N)了,这个后续再研究把。但是整体来说,比暴力破解还是快的。

3. 结语

关于KMP算法就先讲到这里,你理解了吗?如果以上内容有什么错误,欢迎大佬不吝赐教。
参考:KMP算法详解-彻底清楚了(转载+部分原创)

  • 29
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值