KMP模式匹配算法JAVA代码实现

一、前言

完整代码在第三段落,不看废话可以直达,第四段是对Next数组的改良:NextVal数组的代码实现,以及我对采用NextVal数组进行连续匹配的一些疑问,如果有大佬知道怎么解决这个问题欢迎评论区帮我解答。最近在复习数据结构(跟着老韩),前天学kmp的时候,代码实现着实有点拉跨,照着文本敲了代码让人摸不着头脑,于是乎找来了放在手边的大话数据结构,求next数组的时候是默认字符串第一位(也就是下标为0的位置)存储字符串长度,这让我有点懵逼,我想着平时字符串第一位我也不会放字符长度吧,就来到了b站看up主的思路,KMP字符串匹配算法1_哔哩哔哩_bilibiliKMP字符串匹配算法2_哔哩哔哩_bilibili。这个up主讲的很好很全了,与其他的讲解不同的是,他求的next数组是当前位置的相同公共子串的长度,最终全部后移一位,第一位置为-1,原本的最后一位舍弃,想知道kmp具体怎么实现的看完上方的两个视频肯定就懂了,这篇文章具体针对其中的一些边界情况的代码问题修改一下。

二、代码的一些问题

1、先上up主的代码:

  • 首先肯定是先求前缀表了(注意这里前缀表全部后移一位,然后将第一位置为-1),具体的思路都在上方视频链接中,这边我就不赘述了。

    • 前缀表后移
    //前缀表后移一位,第一个值设为-1
        public static void movePrefixTable(int prefix[]) {
            int i;
            int n = prefix.length;
            for (i = n - 1; i > 0; i--) {
                prefix[i] = prefix[i - 1];
            }
            prefix[0] = -1;
        }
    
    • 前缀表具体求解代码
        //获得模式串的前缀表
        public static void getPrefixTable(String pattern, int prefix[]) {
            prefix[0] = 0;//前缀表第一位永远是0
            int k = 0;//k为上一个字符的对称程度,如果k=0说明不再有子对称
            int i = 1;//因为第一位的prefix永远是0,所以从第二位开始计算
            int n = pattern.length();//模式串的长度
            while (i < n) {
                if (pattern.charAt(k) == pattern.charAt(i)) {//找到了这个子对称,或者是直接继承了前面的对称性,这两种都在前面的基础上++
                    k++;
                    prefix[i] = k;
                    i++;
                } else {
                    if (k > 0) {
                        k = prefix[k - 1];//寻找更小的对称程度
                    } else {//如果遍历了所有子对称都无效,说明这个新字符不具有对称性,清0
                        prefix[i] = k;
                        i++;
                    }
                }
            }
            movePrefixTable(prefix);//得到最终的前缀表
    
        }
    }
    
  • 求完前缀表,当然是具体的kmp算法咯

        //kmp模式匹配算法
        public static void kmp(String S, String T, int pos) {
            int i = pos;//i用于存储主串S中当前位置的下标值,从pos位置开始匹配
            int j = 0;//j用于存储子串T中当前位置的下标值
            int[] prefix = new int[T.length()];
            getPrefixTable(T, prefix);
    
            while (i <= S.length() - 1) {//当i小于(S的长度 - 1)并且j小于(T的长度 - 1),循环继续
                if (j == T.length() - 1 && S.charAt(i) == T.charAt(j)) {
                    System.out.println("找到匹配字符index:" + (i - j));
                    j = prefix[j];
                }
    
                if (S.charAt(i) == T.charAt(j)) {//检测到有相等的字符,就继续
                    ++i;
                    ++j;
                } else {
                    j = prefix[j];
                    if (j == -1) {
                        i++;
                        j++;
                    }
                }
            }
        }
    

2、运行时的问题

  • 这是我的main方法测试函数

    public static void main(String[] args) {
            String str1 = "真da真真真真帅 韩帅比真帅";
            String str2 = "真";
            kmp(str1, str2, 0);
    
    
            String str3 = "真真a真真真帅 韩帅比真帅";
            String str4 = "真真";
            kmp(str3, str4, 0);
    
            String str5 = "ABABABAACABAACABAAA";
            String str6 = "ABAACABAA";
            kmp(str5, str6, 0);
        }
    

    当我们运行的时候就会发现第一种情况,程序会报出越界异常String index out of range: -1,这是因为字符串prefix[0] = -1,当找到模式串对应的子串后就会返回 j = prefix[j] = -1,这时候下一组再比较就会出现越界情况了。

    所以我在找到匹配子串输出语句并重置j值之后增加了一个if语句if (j == -1) {j++;},但是这样,第一种情况就会越界String index out of range: 1,越界方式不同了而已。

    然后我想到既然匹配到了子串,模式串长度大于等于2的完全可以继续这个循环,他最多重置j值为0,所以不会出错。但长度为1的模式串,找到了匹配子串,j值重置为-1,我们首先需要将其置为0,然后还要将主串的比较的位置后移,才能继续比较,并且还需要使用continue语句进入下一个循环,不然如果在匹配主串为"AA",模式串为"A"的情况时,A匹配到第一个A,j变为0,继续循环,再次匹配到,++j,此时会越界String index out of range: -1。所以增加代码:

    if (T.length() == 1) {
                        i++;
                        j++;
                        continue;
                    }
    

    这里要注意必须是模式串长度为1才可执行此代码,否则会丢失某些匹配的子串,如用"ABAACABAA"寻找"ABABABAACABAACABAAA"中匹配的子串,会丢失第二个匹配的子串。

    三、最终代码

    package DataStructures.String;
    
    import java.util.Arrays;
    
    /**
     * KMP算法
     *
     * @author 韩帅比
     * @create 2021-08-23 19:40
     */
    public class KMP {
        public static void main(String[] args) {
            String str1 = "真da真真真真帅 韩帅比真帅";
            String str2 = "真";
            kmp(str1, str2, 0);
    
    
            String str3 = "韩帅比 大帅逼真帅逼真帅 韩帅比真帅";
            String str4 = "帅";
            int[] prefix = new int[str4.length()];
            getPrefixTable(str4, prefix);
            System.out.println(Arrays.toString(prefix));
            kmp(str3, str4, 0);
    
            String str5 = "ABABABAACABAACABAAA";
            String str6 = "ABAACABAA";
            int[] prefix2 = new int[str6.length()];
            getPrefixTable(str6, prefix2);
            System.out.println(Arrays.toString(prefix2));
            kmp(str5, str6, 0);
    
        }
    
        //kmp模式匹配算法
        public static void kmp(String S, String T, int pos) {
            int i = pos;//i用于存储主串S中当前位置的下标值,从pos位置开始匹配
            int j = 0;//j用于存储子串T中当前位置的下标值
            int[] prefix = new int[T.length()];
            getPrefixTable(T, prefix);
    
            while (i <= S.length() - 1) {//当i小于(S的长度 - 1)并且j小于(T的长度 - 1),循环继续
                if (j == T.length() - 1 && S.charAt(i) == T.charAt(j)) {
                    System.out.println("找到匹配字符index:" + (i - j));
                    j = prefix[j];
                    if (T.length() == 1) {
                        i++;
                        j++;
                        continue;
                    }
                }
    
                if (S.charAt(i) == T.charAt(j)) {//检测到有相等的字符,就继续
                    ++i;
                    ++j;
                } else {
                    j = prefix[j];
                    if (j == -1) {
                        i++;
                        j++;
                    }
                }
            }
        }
    
    
        //前缀表后移一位,第一个值设为-1
        public static void movePrefixTable(int prefix[]) {
            int i;
            int n = prefix.length;
            for (i = n - 1; i > 0; i--) {
                prefix[i] = prefix[i - 1];
            }
            prefix[0] = -1;
        }
    
    
        //获得模式串的前缀表
        public static void getPrefixTable(String pattern, int prefix[]) {
            prefix[0] = 0;//前缀表第一位永远是0
            int k = 0;//k为上一个字符的对称程度,如果k=0说明不再有子对称
            int i = 1;//因为第一位的prefix永远是0,所以从第二位开始计算
            int n = pattern.length();//模式串的长度
            while (i < n) {
                if (pattern.charAt(k) == pattern.charAt(i)) {//找到了这个子对称,或者是直接继承了前面的对称性,这两种都在前面的基础上++
                    k++;
                    prefix[i] = k;
                    i++;
                } else {
                    if (k > 0) {
                        k = prefix[k - 1];//寻找更小的对称程度
                    } else {//如果遍历了所有子对称都无效,说明这个新字符不具有对称性,清0
                        prefix[i] = k;
                        i++;
                    }
                }
            }
            movePrefixTable(prefix);//得到最终的前缀表
    
        }
    }
    

    四、NEXT数组改良,NEXTVAL数组

  • 下面是对Next数组的改良,NextVal数组的代码

        private static int[] getNextVal(String T, int nextVal[]) {
            int len = T.length();
            nextVal[0] = -1;
            int i = 0;
            int k = -1;
            while (i < len - 1) {
                if (k == -1 || T.charAt(i) == T.charAt(k)) {
                    ++k;
                    ++i;
                    
                    if (T.charAt(i) != T.charAt(k)) {
                        nextVal[i] = k;
                    } else {
                        nextVal[i] = nextVal[k];
                    }
                    
                } else {
                    k = nextVal[k];
                }
            }
            return nextVal;
        }
    

    NextVal数组在匹配第一个相等子串的时候增加了效率,但在连续匹配的时候却会丢失匹配,比如"ABABABAACABAACABAAA"匹配"ABAACABAA"时,原Next数组为{-1,0,0,1,1,0,1,2,3},所以在匹配到第一个对应的子串之后,回退到模式串第四位即第三个A处,即可继续匹配,但NextVal数组为{-1,0,-1,1,1,-1,0,-1,1},匹配到第一个对应的子串后会回退到模式串的第二位,即第一个B,这样就匹配不到紧随其后的第二个对应的子串了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值