KMP算法

KMP算法介绍

  • KMP名字的来源,因为该算法是由D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。

  • 该算法主要用于字符串匹配问题

  • 假设主串长度n,模式串(主串相匹配的字符串)长度为m,如果使用暴力匹配算法,由于指针回溯的问题,最差的情况下的时间复杂度为O(m*n)

  • KMP算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。

字符串最长公共前缀后缀

在说公共前后缀之前先说一下前缀和后缀:

  • 前缀:指除了最后一个字符以外,一个字符串的全部头部组合
  • 后缀:指除了第一个字符以外,一个字符串的全部尾部组合

光说概念有点晦涩,举个例子:假设字符串为ABCDAB

  • 前缀组合为:[A, AB, ABC, ABCD, ABCDA]
  • 后置组合为:[BCDAB, CDAB, DAB, AB, B]
  • 所谓的公共前后缀,就是指后缀和前缀相同,从上面的两个组合中可以发现,相同的只有是AB,所AB即是公共前后缀,也是最大公共前后缀。

next数组

假设一个字符串为:ABAABCA

  • 找出该字符串的各个子串的公共前后缀
字符串子串前缀后缀最长公共前后缀
A0
ABAB0
ABAA,ABBA,A1
ABAAA,AB,ABABAA,AA,A1
ABAABA,AB,ABA.ABAABAAB,AAB,AB,B2
ABAABCA,AB,ABA,ABAA,ABAABBAABC,AABC,ABC,BC,C0
ABAABCAA,AB,ABA,ABAA,ABAAB,ABAABCBAABCA,AABCA,ABCA,BCA,CA,A1
  • 这样,最长公共前后缀就会和串的每个字符产生一种对应关系:
子串ABAABCA
公共前后缀长度0011201

这个表的含义是:当前字符作为最后一个字符时,当前子串所拥有的公共前后缀最长长度

接下来我们就用这个表来引出next数组,next 数组的值是除当前字符外(注意不包括当前字符)子串的公共前后缀最长长度,相当于把上表做一个变形,将表中公共前后缀最长长度全部右移一位,第一个值赋为-1。

字符ABAABCA
next-1001120

上面是我们手推导的,我们如何使用代码求取这个next数组那,先分析一下思路:

在这里插入图片描述

  • 1,next[0]=-1,我们用不到这个值,因为不包含A的子串就是空串,更不用说公共前后缀,所以把它设为-1,其他值也可以。

  • 2,next[1]=0,单个字符没有所谓的前缀和后缀。

接下来才是我们需要求解的:i从0开始,j从1开始,i小于j;

  • 3,求解next[2]的值

在这里插入图片描述

i=0;j=1;
T[i]!=T[j];
next[2]=0;

二者不等表明:每一个子串的最大公共前后缀不可能再相等了,从上面那个表中我们也能看出来,所以比较次大前缀和后缀,然后将j加1,i的指针回退:i=0

  • 4,求解next[3]的值

在这里插入图片描述

i=0;j=2;
T[i]=T[j];
next[3]=1;

此时,表明次大的前缀后缀是可以相等的,因此将二者同时加1

  • 5,求解next[4]的值

在这里插入图片描述

i=1;j=3;
T[i]!=T[j];

此时表明:次大的前后缀相等的情况到此为止,从这个位置向后位置的次大前后缀都不可能相等了,此时需要考虑三级前后缀是否相等了,指针i回退,i=0,j=3,我们先这样回退,等到后面在分析,其实这样回退并不太好。

i=0;j=3;
T[i]=T[j];
next[4]=1;

注意:三级公共前后缀相等,以后的情况就不需要判断一级和二级公共前后缀了,因此同时将二者加1

  • 6,求解next[5]的值

在这里插入图片描述

i=1,j=4
T[i]=T[j];
next[5]=2;

以后的三级公共前后缀还可能相等,因此将两者都加1

  • 7,求解next[6]的值
    在这里插入图片描述
i=2,j=5
T[i]!=T[j];

从这位置开始,三级公共前后缀相等的情况也不存在了,指针回退,i=0,j=4,直接判断四级公共前后缀

i=0,j=4
T[i]!=T[j];

从这位置开始,四级公共前后缀相等的情况也不存在了,指针回退,i=0,j=5,直接判断五级公共前后缀

i=0,j=5
T[i]!=T[j];
next[6]=0;

通过上面的分析,虽然公共前后缀能求出来,但是用程序实现出来肯定是很麻烦的,因为一旦出现不匹配的情况,指针i和j都在回退,代码也很难实现,我们可以利用前面已经匹配的结果,这样就避免重复的比较,我们再来分析一下:
在这里插入图片描述next数组实现代码:

public static int[] GetNext(String str) {
        //将字符串转换为字符数组
        char [] chars = str.toCharArray();
        //定义next数组,长度和传入的模式串一样
        int[] next=new int[str.length()];
        //定义两个索引变量
        int j=0,i=-1;
        //next[0]=-1
        next[j]=i;
        //因为每次循环里面执行j++,然后在赋值,索引不减一会索引越界
        //第一轮循环:给next[1]=0,从第二轮循环才真正有意义
        while(j<str.length()-1){
            if(i==-1||chars[j]==chars[i]){
                j++;
                i++;
                next[j]=i;//用于记录匹配的最大公共前后缀的值
            } else{
                i=next[i];//匹配不成功回溯,使用之前已经匹配的结果
            }
        }
        return next;
    }

KMP算法代码实现

前面大费周章的说next数组,现在就要派上用场了,我们在做字符串匹配的时候,先求出模式串对应的next数组,然后使用模式串匹配主串的时候,回溯就不用从头回溯,而是使用next数组中的值进行回溯,原因和分析next数组是一样的,因为匹配成功的串必然是相等的,所以它们拥有相同的公共前后缀,回溯自然可以利用前面已经匹配的结果,这也是KMP算法和BF(暴力匹配)的区别:

  • 注意:如果第一个字符串就没有匹配成功,我们就采用和BF相同的处理方式:加一
 public static int KMP(String mainStr,String patternStr){
        //求出next数组
        int[] next =GetNext(patternStr);
        //将字符串转换为字符数组
        char [] mainChar = mainStr.toCharArray();
        char[] patternChar=patternStr.toCharArray();
        //定义两个索引变量,i代表主串,j代表模式串
        int i=0,j=0;
        //退出循环的条件为,索引越界,i单独越界匹配失败,j单独越界表示匹配成功,同时越界表示匹配成功
        while (i<mainChar.length && j<patternChar.length) {
            //匹配成功:也就是mainChar[i]==patternChar[j],索引向后加1
            //如果索引回溯到j=next[0]=-1:索引直接加1,也就是采用和BF相同的方法直接后移
            if (j==-1 || mainChar[i]==patternChar[j]) {
                //i,j各增1
                i++;
                j++;
            } else {
                //从串的索引回溯,主串索引不回溯,这个和求取next数组的原理是一样的
                // 避免双回溯,使用之前比较过的结果
                //注意:如果第一个字符就匹配失败,就会出现j=next[0]=-1的情况
                j=next[j];
            }
        }
        // 判断是不是匹配成功,也就是索引j越界
        if (j>=patternChar.length)
            return(i-j);  	//返回匹配模式串的首字符下标
        else
            return(-1);        		//返回不匹配标志
    }

完整代码

public class Test01 {
    public static void main(String[] args) {
        String mainStr="ABAABABAABAABCA";
        String patternStr="ABAABCA";
        int num=KMP(mainStr,patternStr);
        for(int i=0;i<patternStr.length();i++){
            System.out.print(mainStr.charAt(num+i));
        }
    }
    public static int[] GetNext(String str) {
        int[] next=new int[str.length()];
        //定义两个索引变量
        int j=0,i=-1;
        //next[0]=-1
        next[j]=i;
        //因为每次循环里面执行j++,然后在赋值,索引不减一会索引越界
        //第一轮循环:给next[1]=0,从第二轮循环才真正有意义
        while(j<str.length()-1){
            if(i==-1||str.charAt(j)==str.charAt(i)){
                j++;
                i++;
                next[j]=i;//用于记录匹配的最大公共前后缀的值
            } else{
                i=next[i];//匹配不成功回溯,使用之前已经匹配的结果
            }
        }
        return next;
    }
    public static int KMP(String mainStr,String patternStr){
        //求出next数组
        int[] next =GetNext(patternStr);
        //定义两个索引变量,i代表主串,j代表模式串
        int i=0,j=0;
        //退出循环的条件为,索引越界,i单独越界匹配失败,j单独越界表示匹配成功,同时越界表示匹配成功
        while (i<mainStr.length() && j<patternStr.length()) {
            //匹配成功:也就是mainChar[i]==patternChar[j],索引向后加1
            //如果索引回溯到j=next[0]=-1:索引直接加1,也就是采用和BF相同的方法直接后移
            if (j==-1 || mainStr.charAt(i)==patternStr.charAt(j)) {
                //i,j各增1
                i++;
                j++;
            } else {
                //从串的索引回溯,主串索引不回溯,这个和求取next数组的原理是一样的
                // 避免双回溯,使用之前比较过的结果
                //注意:如果第一个字符就匹配失败,就会出现j=next[0]=-1的情况
                j=next[j];
            }
        }
        // 判断是不是匹配成功,也就是索引j越界
        if (j>=patternStr.length())
            return(i-j);  	//返回匹配模式串的首字符下标
        else
            return(-1);        		//返回不匹配标志
    }
}

拓展

其实KMP算法还可以继续优化,由于时间和精力有限,就不做探讨了,,,,,
有精力的小伙伴可以参考这篇博客:传送门

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彤彤的小跟班

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值