KMP算法详解(保姆级别)

KMP算法详解(保姆级别)

这东西没那么难理解,看别人的文章写的有点啰嗦了

自己会的东西不算会,能给别人讲明白才算会

背景

  • 给你两个字符串,问你他们是否具有包含关系(连续的)!大部分都会首先想到暴力解法,一个个比较!直到找到比对上的!但是如果是这两个:aaaaaaabaaaaaaac就很操蛋了(O(n*m))!里面包含很多重复比较的过程,如何设计一种算法来减少这种重复很有必要!

这就是KMP研究的领域(加速BUFF)!不过想要彻底弄明白KMP;还得先搞懂一个最大前缀数组

最大前缀数组(最长前缀&&最长后缀)

假定一个字符K,我们给它赋予一个信息,

那么这个next数组放什么东西呢?------最大相同前后缀!举个例子

  • abbcabbk------对于K前面的字符串:abbcabb;相同的前后缀有哪些?(前n个和后n个相同)显然:abb,所以next={a,b,b};注意:不包括自身如abbcabb,如果有多个!选最长的!实际上我们只需要长度信息就可以了!就像next[k]=3;

比方说:字符串“ababc”的next={-1,0,0,1,2};第一个a前面没有元素,则为-1;

在代码中如何体现呢?很简单啦!设计个小小的算法!

具体如何实现,你就先把它当作一个可用的API把KMP解决后再回过头来研究它如何设计!

给定两个字符串 判断str1中是否包含str2字符串!

str1="abbtcfabbtkadd······"

str2="abbtcfabbtu······"str2中的每个字符都有next信息!

str1abbtcfabbtka···
str2abbtcfabbtuv···
step2:str1××××××abbtka
str2abbtc
step3:str1×××××××××××a
str2abbtcfabbtuv···
  • 第一步:传统比较----str1[i] 比较 str2[i],比较到最后一个元素–>k != u 比对失败;跳到下一次比较!

  • 第二步:传统是从str[1]重复第一步!显然 太慢了,这时候就需要KMP闪亮登场!拿到第一步比对的最后一个元素!—u(其中包含其最长前缀数组**【abbt】**),这一次比对str1不用回到str1[1],而是就停在原地----->k;那么str2从哪里开始和他比对呢?

    仔细看看k前面的元素:abbt,这是什么?这不就是str2中u的前缀数组吗?所以str2就跳到最长前缀abbt的后一个开始比对!c

    step2:str1××××××abbtka
    str2abbtc
  • 第三步:显然:k != c;比对失败;重复第二步!c的前缀数组显然为空了,也就是长度为0;str2表示自己已经没有选择的余地了!只能让str1选了!str1只能向后移动:选k的下一个元素和str2重复一二三步!

step3×××××××××××a
abbtcfabbtuv···

理论存在,动手敲代码!这里使用Java实现!

  • 数组比对,索引必不可少!首先定义i1、i2两个索引对应str1、str2;
  • 拿到str2每个元素的最大前缀数的数组:getNextArray(str2);
  • 循环判断,条件是i1、i2不越界:i1 < str1.length() && i2 < str2.length()
  • 三种情况对应上面三步;
    • 1、比对上了:i1++;i2++;
    • 2、比对不上;i2跳到最大前缀后一个:i2 = next[i2];
    • 3、2能不能跳还得看有没有前缀:没有了就不能跳了也就是i2 == 0,这时候就得让i1++
 
public static int kmp(String str1,String str2){
        //终止条件
        if (str1 == null || str2 == null || str1.length() < str2.length()){
            return -1;
        }
        char[] str11 = str1.toCharArray();
        char[] str22 = str2.toCharArray();
        int i1 = 0;
        int i2 = 0;
        int[] next = getNextArray(str22);
        //循环条件,i1\i2不可越界
        while(i1 < str1.length() && i2 < str2.length()){
            if (str11[i1] == str22[i2]){
                i1++;
                i2++;
            }else if (i2 == 0){//没有前缀可以跳了
                //就让i1向后挪动
                i1++;
            }else {
                //让i2移动到前缀后面一位,也就是next[i2]
                i2 = next[i2];
            }
        }
        //返回条件:i2是不是到最后一个元素了?
        return i2 == str22.length ? i1 - i2 : -1;
    }

最长前缀树算法

我自己思考半天写出一个最初版本:

使用的是暴力解法,每个字符都得找出他的前缀长度,这得需要一个for循环!对于这个元素,找它最大可能的前缀长度,就先求最长的时候!max = i(当前元素数组下标) - 1,min = 1;这又是一个循环;这里使用while,定义一个变量x从1到i-1递增!

最后判断这个长度对应的前后缀是否相等,又是一个for循环!循环区域从0~i-x-1;前缀和后缀元素不相等就这一轮报废!到下一轮,X+=1;如果全部都相等了(最后一个也相等),就不用进行下一轮了,因为这一轮的结果就是最大的!让x = i跳出while循环进行下一个元素的计算!

//找到该字符串的最大相等前后缀
    private static int[] getNextArray(char[] str22) {
        if (str22.length <= 1){
            return new int[]{-1};
        }
        //判断前n和后n是否相等
        int[] arr = new int[str22.length];
        //数组的第一第二个一定是-1和0;不会变的
        arr[0] = -1;
        arr[1] = 0;
        //遍历剩下的元素!
        for (int i = 2; i < str22.length; i++) {
            int x  = 1;//第一轮···对应最大长度
            while (x <= i-1){//总的论数:i-1
                //这一轮的元素长度
                for (int j = 0;j<=i-x-1;j++){
                    if (str22[j] != str22[j+x]){
                        x += 1;
                        break;
                    }else if (str22[i-x-1] == str22[i-1]){//全部相等,找到最大长度
                        arr[i] = i-x;
                        x = i;
                    }
                }
            }
        }
        return arr;
    }

可以说是很容易理解!但是!自己写的永远不是最佳方案!下面介绍更高级的解法!

首先:next数组的第一第二元素肯定是规定好了的-1和0这毋庸置疑!那么我们到第三个元素了的时候能不能利用第二个元素的信息呢?

可以看到,我们直接利用前一个元素的信息,在它的基础上进行比较!如果前一个位置str[i-1] == str[next[i-1]], ==str[next[i-1]]为i-1的最长前缀的下一个元素,与最长后缀的下一个元素(i-1)str[i-1]==进行比较!那么就能推出:next[i] = next[i-1] + 1;

如果不等于呢?把i-1变成next[next[i-1]], 用它的信息(也就是不等与的这个位置)重来一次!

代码:

private static int[] getNextArray2(char[] str22) {
        if (str22.length <= 1){
            return new int[]{-1};
        }
        //判断前n和后n是否相等
        int[] arr = new int[str22.length];
        arr[0] = -1;
        arr[1] = 0;
        //next数组的位置
        int i = 2;
        //需要一个变量进行跳转;
        int ct = 0;
        while(i < arr.length){
            //如果前一个元素等于最长前缀的后一个
            if (str22[i-1] == str22[ct]){
                arr[i++] = ++ct;
            }else if (ct > 0){
                ct = arr[ct];
            }else {
                arr[i++] = 0;
            }
        }
        return arr;
    }

三个条件分支是不是有点像KMP的三个条件分支,所以KMP难得不是它本身,而是计算前缀数组信息

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值