数据结构(Java)KMP算法

一、应用场景-字符串匹配问题

1) 有一个字符串 str1= "BBC ABCDAB ABCDABCDABDE ",和一个子串 str2="ABCDABD " 
2) 现在要判断 str1 是否含有 str2 , 如果存在,就返回第一次出现的位置 , 如果没有,则返回 -1

二、解决方式-暴力匹配

        如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则有:

1) 如果当前字符匹配成功(即 str1[ i ] == str2[j] ),则 i ++ j++ ,继续匹配下一个字符
2) 如果失配(即 str1[ i ]! = str2[j] ),令 i = i - (j - 1) j = 0 。相当于每次匹配失败时, i 回溯 j 被置为 0
3) 用暴力方法解决的话就会有大量的 回溯 ,每次只移动 一位 ,若是不匹配,移动到下一位接着判断,浪费了大量的时间。 ( 不可行 !)
  public static int violenceMatch(String source, String target) {
        int i = 0, j = 0;
        char[] s1 = source.toCharArray();
        char[] s2 = target.toCharArray();
        //任意一个索引越界了就结束 一般i越界了有可能没找到,j越界了是找到了
        while (i < source.length() && j < target.length()) {
            count++;
            //第一个字符匹配成功,接着往下面匹配
            if (s1[i] == s2[j]) {
                i++;
                j++;
            } else {
                //匹配不成功,从下一个位置重新开始匹配
                i = i - j + 1;
                j = 0;
            }
            //j指向了目标字符串的最后,匹配成功了
            if (j == target.length()) return i - j;
        }
        return -1;
    }

三、解决方式-KMP算法

1) KMP 是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法
2) Knuth-Morris-Pratt 字符串查找算法 ,简称为 “ KMP 算法”,常用于在一个文本串 S 内查找一个模式串 P 的出现位置,这个算法由 Donald Knuth Vaughan Pratt James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法 .
3) KMP 方法算法就利用之前判断过的信息,通过 一个next数组 ,保存模式串中前后 最长公共子序列 长度 ,每次回溯时,通过 next 数组找到,前面匹配过的位置,省去了大量的计算时间。
        其实kmp算法与暴力算法的区别在于暴力算法每次匹配只后移一位,而kmp算法利用next数组动态地计算每次匹配后移的位数。next数组利用目标字符串匹配前就已得出。

   

3.1 前缀与后缀

3.2 next数组与部分匹配值: 

 3.3 举例

假如我们已经匹配到了这里,可以看到下一个字符不匹配。

如果按照暴力方法,下一次匹配的位置应该是最开始匹配位置后一位

利用KMP算法,计算出下一次后移的位数是 4 位:

已匹配字符数(6)- 最后一个已匹配字符的部分匹配值(2))

 四、KMP的代码演示

1、首先是获取next数组,这个是网上的版本,我不理解。我在主程序中用别的思路实现了同样的结果

 //获取next数组的网上版本,我看不懂
    public static int[] Next1(String s) {
        int[] next = new int[s.length()];
        next[0] = 0;//第一个元素无论如何都是0,不跟自己匹配
        //索引后移
        for (int i = 1, k = 0; i < s.length(); i++) {
            while (k > 0 && s.charAt(k) != s.charAt(i)) k = next[k - 1];//
            if (s.charAt(k) == s.charAt(i)) k++;//如果某次匹配到了,就让匹配长度+1
            next[i] = k;//不论每次是否匹配到都把k赋给next的对应元素,也就是匹配长度
        }
        return next;
    }

2.主程序

注:next数组的思路:

      1.  i指向字符串首位,j指向字符串首位的后一位j 不断后移,直到遇到和首位相同的字符时 i、j同时后移,这时同时记录匹配长度next数组中。

      2. 一旦又遇到了不同的字符,这里要判断了: i 回到字符串首位,引入右指针 k=j 。i、k此时是一对对撞指针。移动 i、k(移动次数小于匹配长度),判断所指元素是否相等,以验证之前的匹配长度是否有效。

加入 2.1 这一步是因为可能有下面情况:

        a a b a a a c

        0 1 0 1 2 ?

此前匹配长度为2,这里 b 与 a不匹配,如果直接清除匹配长度,结果是错的!

        a a b a a a 最大匹配长度应该是 2(aa),而不是0

我们应该利用对撞指针,判断之前的匹配长度是否还有效,类似于回文串的判断,对撞指针移动次数就是匹配长度-1

        2.1 若匹配都成功,记录此次匹配长度,i 不需要再移到首位,等待下一次匹配。

        2.1 若匹配失败,i 就回到字符串首位,清空 匹配长度变量,等待下一次匹配。

      3. j始终后移。如果又匹配成功了,就重复类似于 1 的操作(i、j同时后移)。直到 j 指到了字符串末尾

public static Integer count=0;//匹配次数计数,对比两种方法
public static int KMPMatch(String source,String target){
         //1、获得next数组,这是kmp算法的关键,这样我们才能决定下次从哪里开始匹配
         int[] next = new int[target.length()];
         int matchedLength = 0, i = 0, j = 1;
         next[0] = 0;//第一个元素无论如何都是0,不跟自己匹配
         //i指向字符首位,j后移。
         while (j < target.length()) {
             //如果 i 与 j 所指字符匹配成功,就同时后移,同时记录匹配的字符长度
             if (target.charAt(i) == target.charAt(j)) {
                 next[j] = ++matchedLength;
                 i++;
                 //否则就把 i 移到首位,进行判断
             } else{
                i=0;//重置指针
                int k=j;//记录当前的后指针
                //这里要额外的判断:利用对撞指针,验证之前的匹配长度是否还适用
                while(i<k && i<matchedLength ){
                    //某次匹配失败,即验证之前的匹配长度失效
                    if(needle.charAt(i)!=needle.charAt(k)){
                        next[j]=0;
                        matchedLength =0;//重置匹配长度
                        i=0;//重置指针
                        break;
                    }else{
                        i++;//移动对撞指针(左右)
                        k--;
                    }
                }
                //如果验证成功,就记录先前的匹配长度
                next[j]=matchedLength;//记录后指针的最长匹配长度
            }
             //j始终向后遍历
             j++;
         }
         //2、真正的匹配过程
         int k = 0, l = 0;//k指向源字符串,l指向目标字符串
         int matched=0;
         char[] s1 = source.toCharArray();
         char[] s2 = target.toCharArray();
         //任意一个索引越界了就结束 一般i越界了有可能没找到,j越界了是找到了
         while (k < source.length() && l < target.length()) {
             count++;
             //第一个字符匹配成功,接着往下面匹配
             if (s1[k] == s2[l]) {
                 k++;
                 l++;
                 matched++;

             } else {
                 //匹配不成功,从后面位置重新开始匹配,这个位置经过了kmp算法的优化
                 if(matched>=1) k=k-l+matched-next[matched-1];//字符长度不为0,那就根据算法选择下次开始比较的位置
                 else k=k-l+1;//字符长度为0,那就跟暴力方法一样回到原来位置的下一位
                 l = 0;
                 matched=0;
             }
             //l指向了目标字符串的最后,匹配成功了
             if (l == target.length()) return k - l;
         }
         return -1;
     }

五、最终对比

@Test
    public void testViolenceMatch() {
        String source = "BBC ABCDAB ABCDABCDABDE";
        String target = "ABCDABD";
        System.out.println(solution.violenceMatch(source, target));
        System.out.println("匹配次数:"+solution.count);
    }



15
匹配次数:36



    @Test
    public void testKMPMatch() {
        String source = "BBC ABCDAB ABCDABCDABDE";
        String target = "ABCDABD";
        System.out.println(solution.KMPMatch(source,target));
        System.out.println("匹配次数:"+solution.count);
    }



15
匹配次数:29

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值