KMP算法-求一个字符串是否包含另一个子串

KMP算法

KMP算法是干什么的?

KMP算法可以认为是一种字符串的查找算法,或者说是匹配算法。给大家举个例子,比如说有一个字符串  str1 = abc123def ,str2 = 123,str1是否有某一个子串(连续的)和str2一样, 有,返回str1中第一次出现str2中起始字符的位置。

面对这样一个问题,我们先不说KMP算法,我们先说一个麻烦的算法。我们再str1从0开始依次匹配str2,能匹配上就返回,匹配不上就下一个位置继续。对于这整个一个暴力的过程,str1 = aaaaaaaaab,str2 = aaab,我们从0开始遍历,0-6位置都遍历了s2长度次数才宣告失败,所以时间复杂度在最差情况下为O(N*M) N为str1的长度 M为str2的长度。而使用KMP算法可以做到时间复杂度为O(N)。

KMP算法是一种比较难的算法,所以在了解KMP算法之前呢,我们需要奠定一些基本的概念。

1.什么是前缀与后缀串的最长匹配长度?

我们还是用例子说话,比如说  abcabck 首先,这个信息是根据位置来分的,也就是说每一个位置都有一个该信息,我们拿6位置的k举例,该信息代表不包含该位置字符前缀与后缀最长的匹配长度。该位置的信息和该位置字符没有任何关系,它跟该位置之前的字符有关。6位置之前字符为abcabc,在不包含自身字符的情况下,最长前缀和最长后缀相等均为 abc  长度为3 ,所以6位置的信息为 3,再来一个例子 aaaaak,那么k位置的信息为 4,前缀后缀均为aaaa,我们不能取5,因为刚刚说了,不能包含字符本身。再举一个例子 aabaabs 每一个字符所对应的信息为[-1,0,1,0,1,2,3] 对于0位置和1位置,我们人为规定,该位置就是-1和0,怎么解释,0位置,之前没有字符串,就没有不包含该位置的前缀与后缀,我们设置为 -1 ,而1位置呢,1位置前面有一个0位置,但我们说了,不能包含该字符串本身,所以就是0,其他的就按上述规则填写即可。上述的一个位置信息数组,就是KMP算法的核心,我们称之为next数组

2.KMP算法核心,next数组。

上述例子不是有一个str1 和 str2 嘛,没错,这个next数组就是对str2求的。也就是说,我们如果要找str1中是否包含str2,我们需要先求出str2的一个next数组。

3.我们为什么需要str2的next数组?

因为它可以使我们整个匹配的过程加速。

4.怎么加速我们的匹配过程?

 我们先假设str2已经拥有了next数组。

然后我们假设一个str1  前 .....  都没和str2的字符匹配上,然后突然再i位置和str2的0位置字符匹配上了,然后一直都和str2匹配上了,知道s1到了x为止,s2到了y为止才没有匹配上,这个时候注意,我们的str2是不是都有一个信息,我们的y为止是不是存储了一个最长的前缀和最长的后缀的长度。我们再str1中找到和str2后缀串相同的长度,假如说我们再str1中叫j位置,然后我们把str2中0位置往后推,推到和j位置齐平。如下图

太长时间没写过字了,大家尽量看,是不是稍微的好理解一些。

实质一:

类似于 a = b ,b = c  所以 a = c 的实质。因为前缀串和后缀串相等,而后缀串和 j....x  相等,所以前缀串和 j.....x 相等,所以这部分就不需要重新匹配了,直接跳过,有一个小的加速。

实质二:

我们直接从 j 位置开始往后匹配了, 那 i+1位置呢, i+2位置呢, i+3位置呢,通过next数组,我们可以知道,其实这些位置都不可能匹配出str2,所以,这些位置也直接舍弃,又有一段小加速。

接下来来一个实际的例子。

s1               ..............aabaat.....................

s2                             aabaab

我们b底下的信息是2,代表最长前缀aa 和最长后缀  aa  然后我们找到s1中对应的j位置,

s1               ..............aabaat.....................

s2                                   aabaab

接下来我们再这么比,看是不是一下子加速很多。比暴力算法少了很多比较,这就完了嘛,没有,你看b和t是不是也没对上啊,这个时候,我们的b是不是记的也有信息啊,我们拿出这个时候第一个b的信息。这个b的信息是啥呢,是1对不对 ,前缀为 a 后缀为 a 不能包含字符串本身,我们继续移。

s1               ..............aabaat.....................

s2                                     aabaab

然后t和第二个a又没对上,这时我们a的信息记录的是 0 这个时候,我们再往右推一个,第一个a和t 还是没对上,这个时候,a的对应信息是啥啊,是 -1,结束。

这些有部分重的都没对上,s1  t 位置继续往下走,如果遇到有重合的,继续重复该步骤。可以发现KMP算法少了很多重复的步骤。

知道KMP快了,接下来我们来解答一下实质二 为啥跳过了 i + 1 , i  + 2, i + 3。

我们这么来假设,我们假设  i + 1, i + 2, i + 3 ...... j 之前存在一个位置能 匹配上  s2 ,我们假设该位置 为  k, 那么 k ....... x 是不是能搞定 s2 等量的前缀啊,为啥,我们都假设 能匹配出  s2 了,再没有等量的前缀,那么是怎么匹配出s2的。所以我们能推出拥有等量前缀,还有一点,我们是从i ........ x 可是一路相等的啊,我靠,这不就是 y 之前一个等量的后缀嘛,我们推出了y之前一个更长的前缀和后缀。跟我们的next数组中的信息矛盾了,如果我们next数组中信息求解正确,那么这种情况根本就不会发生。所以  i + 1, i + 2, i + 3 .....到 j之前不存在一个能匹配出 s2 的位置。

说了这么多,大家是不是觉得往右推这件事很复杂啊,其实一点都不复杂,还用原来的例子 aabaab 对应的信息  -1 0 1 0 1 2 

a  a   b   a    a    t   

a  a   b   a    a    b    

t 和 b 比对失败了 ,那么往右推这件事啊其实就代表,验证的 t 还在原来位置,而底下验证的位置往 左跳,它跳到哪, b对应的信息就是他跳的位置。其实就是比对的位置,跳到next的值上。

如上图,是不是 b 往其对应的位置跳,就相当于 a 往后右推。很简单吧。

KMP代码:

public class KMP {

    public int getIndexOf(String str1,String str2){
        if (str1 == null || str2 == null || str2.length() < 1 ||str2.length() > str1.length()){
            return -1;
        }

        char[] charsArr1 = str1.toCharArray();
        char[] charsArr2 = str2.toCharArray();
        int[] next = getNextArray(charsArr2);
        int index1 = 0;
        int index2 = 0;

        while (index1 < charsArr1.length && index2 < str2.length()){
            if (charsArr1[index1] == charsArr2[index2]){
                index1++;
                index2++;
            }else if (next[index2] == -1){  //  index2 == 0
                index1++;
            }else {
                index2 = next[index2];
            }
        }
        //如果index2 是越界的,那么一定是index2所有的都匹配上了。所以返回 x位置 - index2
        //如果index2 不是越界的,那么一定是index1越界  index1越界,说明所有都没匹配上。
        return index2 == str2.length() ? index1 - index2 : 0;

    }

    private int[] getNextArray(char[] str2) {
        if (str2.length == 1){
            return new int[]{-1};
        }
        int[] next = new int[str2.length];
        next[0] = -1;
        next[1] = 0;
        int i = 2;    //目前在哪个位置求next数组的值
        int cn = 0;   // cn表示哪个位置的字符 跟 i - 1 位置比
                      // 为啥cn 一开始是 0  因为 当i为 2 时谁和 1比呀,0位置呀,所以是 0
        while ( i < next.length) {
            if (str2[i - 1] == str2[cn]){  //匹配成功的时候
                next[i++] = ++ cn;   //设置了 i 位置该有的值,同时 我也让我下一个位置使用我的信息去完成它的位置
            }else if (cn > 0){
                cn = next[cn];
            }else {
                next[i++] = 0;
            }
        }
        return next;
    }

}

习题:我想知道两个字符串是否互为旋转字符串,啥意思,啥叫旋转字符串,例如 "123456" 我可以不转  "123456"  ,我还可以把 1 转过去 变成 "234561",还可以把 12 转过去变成 "345612" 等等现在给你两个字符串,请你判断是否互为旋转串。

怎么做:

str1 和 str2 先判断长度一样不一样,如果长度不一样,直接false ,因为如果互为旋转串,长度一定一样,我们选择总不能旋转没一个吧。长度一样之后,我们用str1 + str1  生成一个大字符串,就是两个str1拼在一起的大字符串,我们称之为 str1s ,然后我们直接KMP,判断 str2是否是str1s的子串,如果是,原始的str1 和 str2 就互为旋转串。为啥,我们看 123456 拼接上 123456 是啥,是123456123456  在这个大字符串中,任何长度为 6 的子串都为原始串的旋转串。

是不是很简单,代码就是KMP的代码,就不重复写了。

接下来再看另一题。

有两颗树,如下图,我们假设左边的树叫T1右边是树叫T2 问 T1是否有某一个子树的结构和T2一样。如果T1和T2为上面的样子返回false如果为下面的样子,返回true。

 //      3                                         4
 //    /   \                                      /
 //   1     4                                    5
 //    \   / \
 //     2  5  1               F




 //      3                                         4
 //    /   \                                      / 
 //   1     4                                    5   
 //    \   / 
 //     2  5                   T

子树从一个头出发,所有东西都得要,左边以4为头的子树为 4 5 1 跟 4  5 不一样所以上面的返回false。

这道题我们把T1先序或者后续序列化成一个字符串,把T2先序或者后续序列化成一个字符串,然后我们通过KMP发现 T2是T1的子串,则T2是T1的子树。O(n)拿下。

举个栗子:

//                     32                              45
//                   /    \                           /
//                  11    45                         22
//                 /  \   /
//                19  13  22

左边序列化完应该是这样的 ["32","11","19",null,null,13,null,null,"45","22",null,null,null] 这是我们序列化后的结果,这是一个字符类型的数组是吧。我们把整个数组理解为一个字符串,我们把第一个字符认为是 "32"这个字符串,"32"这个字符串,我们就理解为单个字符,"11"这个字符串我们就理解为单个字符。右边 45 22 也一样,序列化后为 ["45","22",null,null,null] 。 然后直接KMP ,怎么样,骚不骚。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值