奇淫巧技的KMP算法--详解

奇淫巧技的KMP算法–详解

花了一下午时间,看了十几个博客,终于拿下了KMP高地,现在总结下下自己对KMP的理解和实现。
情景1
假如你是一名生物学家,现在,你的面前有两段 DNA 序列 S 和 T,你需要判断 T 是否可以匹配成为 S 的子串。
在这里插入图片描述

你可能会凭肉眼立即得出结论:是匹配的。可是计算机没有眼睛,只能对每个字符进行逐一比较。

对于计算机来讲,首先它会从左边第一个位置开始进行逐一比较:
在这里插入图片描述

这样,当匹配到 T 的最后一个字符时,发现不匹配,于是从 S 的第二个字符开始重新进行比较:
在这里插入图片描述

仍然不匹配,再次将 T 与 S 的第三个字符开始匹配…不断重复以上步骤,直到从 S 的第四个字符开始时,最终得出结论:S 与 T 是匹配的。
在这里插入图片描述

你发现这个方法的弊端了吗?我们在进行每一轮匹配时,总是会重复对 A 进行比较。也就是说,对于 S 中的每个字符,我们都需要从 T 第一个位置重新开始比较,并且 S 前面的 A 越多,浪费的时间也就越多。假设 S 的长度为 mmm,T 的长度为 nnn,理论上讲,最坏情况下迭代 m−n+1m - n + 1m−n+1 轮,每轮最多进行 nnn 次比对,一共比较了 (m−n+1)×n(m - n + 1)\times n(m−n+1)×n 次,当 m>>nm >> nm>>n 时,渐进时间复杂度为 O(mn)O(mn)O(mn)。

而 KMP 算法的好处在于,它可以将时间复杂度降低到 O(m+n)O(m + n)O(m+n),字符序列越长,该算法的优势越明显。它是怎么实现的呢?

情景 2
再来举一个例子,现在有如下字符串 S 和 P,判断 P 是否为 S 的子串。
在这里插入图片描述

我们仍然按照原来的方式进行比较,比较到 P 的末尾时,我们发现了不匹配的字符。
在这里插入图片描述

注意,按照原来的思路,我们下一步应将字符串 P 的开头,与字符串 S 的第二位 C 重新进行比较。而 KMP 算法告诉我们,我们只需将字符串 P 需要比较的位置重置到图中 j 的位置,S 保持 i 的位置不变,接下来即可从 i,j 位置继续进行比较。
在这里插入图片描述

为什么?我们发现字符串 P 有子串 ACT 和 ACY,当 T 和 Y 不匹配时,我们就确定了 S 中的蓝色 AC 并不匹配 P 右侧的 AC,但是可能匹配左侧的 AC,所以我们从位置 i 和 j 继续比较。

换句话说,Y 对应下标 2,表示下一步要重新开始的地方。

既然如此,如果每次不匹配的时候,我们都能立刻知道 P 中不匹配的元素,下一步应该从哪个下标重新开始,这样不就能大大简化匹配过程了吗?这就是 KMP 的核心思想。

KMP 算法中,使用一个数组 next 来保存 P 中元素不匹配时,下一步应该重新开始的下标。由于计算机不能像我们人类一样,通过视觉来得出结论,因此这里有一种适合计算机的构造 next 数组的方法。

Next数组求解
好了,到这就来到了核心部分,前面kmp原理介绍就偷了个懒,全文引用了LeetCode 上的KMP原理介绍。KMP的复杂之处,也就是核心之处在于如何求解next数组。求解next数组就需要先理解字符串的最长公共前后*缀。

前缀和后缀定义

前缀:指除了最后一个字符以外一个字符串的全部头部组合。如字符串aabaaac的前缀{a,aa,aab,aaba,aabaa,aabaaa }
后缀:指除了第一个字符以外,一个字符串的全部尾部组合。如字符串aabaaac的前后缀{abaaac,baaac,aaac,aac,ac,c }
next[0]: a 的最长公共前后缀,没有前后缀。为0
next[1]: aa 的最长公共前后缀,前缀{a},后缀{a}。为1
next[2]: aab 的最长公共前后缀,前缀{a,aa},后缀{ ab,b}为0
next[3]: aaba 的最长公共前后缀,前缀{a,aa,aab},后缀{aba,ba,a}。为1
next[4]: aabaa 的最长公共前后缀,前缀{a,aa,aab,aaba},后缀{abaa,baa,aa,a}。为2
next[5]: aabaaa 的最长公共前后缀,前缀{a,aa,aab,aaba,aabaa},后缀
{abaaa,baaa,aaa,aa,a}为2
next[6]: aabaaac 的最长公共前后缀,前缀{a,aa,aab,aaba,aabaa,aabaaa},后缀{abaaac,baaac,aaac,aac,ac,c}。为0
next[i]代表字符串0到i位置(包括i)的子串的最长公共前后缀的长度。

**既next[i] 表示
**str.subString(0,i+1).subString(0,next[i]) == str.subString(0,i+1).subString(i+1-next[i],i+1)这个地方的理解对后面求解next数组很重要

知道next数组的含义之后就是,我们就来求next数组。

 public int[] next(String str){
        int length= str.length();
        int[] next = new int[length];
        int head =1;
        int tial = 0;
        next[0] = 0;
        while(head<length){
            if(str.charAt(head)== str.charAt(tial)){
                tial++;
                next[head] = tial;
                //最长公共前缀后缀的长度,tial+1代表当前最长公共前后缀的长度
                head++;
            }else{
                if(tial==0){
                    //此时代表最长公共前后缀的长度为0;
                    head++;
                }else {
                    tial = next[tial-1];
                }
            }
        }
        return next;
    }

在求next数组的时候,最难理解的就是tial = next[tial-1]; ,这也是KMP算法奇淫巧技的地方,绝大部分的讲解也没有去将为什么处理这步,这也导致了很难去理解KMP算法。
因为str.charAt(tail) != str.charAt(head) (此时最长公共前后缀无法满足增长了,只能是往回找),tial此时需要往回调。
如果tial!=0则表示此时0到tial的位置的子串与head-tial到head位置的子串是一样的。
next[tial-1]表示str.subString(0,tial)这个字符串的最长公共前后缀,而str.subString(0,tial)与str.subString(head-tial,head)是完全一样的,所以tial 跳转 next[tila-1]位置,再将str.charAt(tial) 与str.charAr(head)进行比较,如果相等则next[head] = tial++;tial表示字符的位置。

主要是要理解tial = next[tial-1],巧妙的地方就是tial的往回跳转,当tial!=0时是满足当前head之前有tail-1个元素是与0到tial-1是最长的公共前后缀然后再比较str.charAt(head)== str.charAt(tial)tial==0就表示即到了当前字符串开始位置和最后的head的位置进行比较。

 public int strStr(String haystack, String needle) {
        if(needle.length()==0){
            return 0;
        }
        int []next = next(needle);
        for (int i = 0; i <next.length ; i++) {
            System.out.println(next[i]);
        }
        int S_index = 0;
        int M_index = 0;
        while(S_index<haystack.length()&&M_index < needle.length()){
            if(haystack.charAt(S_index)== needle.charAt(M_index)){
                M_index++;
                S_index++;
            }else{
                if(M_index ==0){
                    //M_index ==0 代表此时在匹配字符串的第一个位置,因
                    // 为needle与haystack第一个位置匹配不上,所以haystack的指针往后移
                    S_index++;
                }else{
                    // M_idnex!=0,根据next的值,needle的指针往回移next[M_index-1]的位置上。
                    M_index = next[M_index-1];
                }
            }
        }
        return M_index == needle.length()?S_index-M_index:-1;
    }

由于不好画图,建议在阅读的过程中画图理解tial = next[tial-1]的跳转思路。

注:也可以BF求解next数组,但是在LeetCode上超时了

public int[] get_Next(String str){
        int length = str.length();
        int[] next = new int[length];
        next[0] = 0;
        for(int i =1;i<length;i++){
            int len =0;
            for(int j =1;j<=i;j++) {
            //枚举前后缀进行比较
                if (str.substring(0, j).equals(str.substring(i + 1 - j, i + 1))) {
                    if (len < j) {
                        len = j;
                    }
                }
            }
            next[i] =len;
        }
        return next;
    }

参考文献:
链接:https://blog.csdn.net/V_0617/article/details/79114860
链接:https://leetcode-cn.com/leetbook/read/array-and-string/cpoo6/
来源:力扣(LeetCode)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值