经典算法 - 图解KMP算法与字符串匹配问题

字符串匹配问题

字符串匹配问题:

  1. 有一个字符串str1 = "BBC ABCDAB ABCDABCDABDE";和一个子串str2 = "ABCDABD";
  2. 现在判断str1是否含有str2,存在返回匹配的第一次位置,不存在返回-1

在这里插入图片描述


暴力匹配法

思路

首先想到的就是暴力匹配法:

  1. 设计两个指针i,j,分别指向两个字符串,i向右移与str2[j]比较

在这里插入图片描述

  1. 一直比较到值相同的位置,i、j后移比较str1[i]str2[j]的值

在这里插入图片描述

  1. 最终比较str1[10]str2[6]不同,不匹配

在这里插入图片描述

  1. 再重新退回str1[5]str2[0]开始比较

在这里插入图片描述

  1. 最终匹配到str1[15]才匹配成功

在这里插入图片描述

Java代码实现

package com.company.十种算法.kmp;

/**
 * Author : zfk
 * Data : 17:14
 * 字符串匹配问题 :暴力匹配法
 */
public class ViolenceMatch {

    public static void main(String[] args) {

        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";

        int index = violenceMatch(str1,str2);
        System.out.println("index = " +index);
    }

    //暴力匹配算法
    public static int violenceMatch(String str1,String str2){

        char[] s1 = str1.toCharArray();
        char[] s2 = str2.toCharArray();

        //i,j为s1,s2的索引
        int i = 0;
        int j = 0;

        //保证不越界
        while (i < s1.length && j < s2.length){
            //匹配成功
            if (s1[i] == s2[j]){
                i++;
                j++;
            }
            else {//没有匹配成功
                //i回到最开始匹配的后一位,j置0
                i = i - (j - 1);
                j = 0;
            }
        }

        //判断是否匹配
        if (j == s2.length){
            return i - j;
        }
        else {
            return -1;
        }

    }

}

暴力匹配法的缺陷

再第3步到第4步时,我们想既然匹配失败,就退回到最开始匹配的地方,继续往后移动i指针

在这里插入图片描述

在这里插入图片描述

实际上是重复计算了,我们肉眼可以看出,str1[4]~str1[7]这一段是可以不用管的,直接到下一个A处

在这里插入图片描述

有什么办法能够实现这个想法呢?就是KMP算法


KMP算法

KMP算法是D.E.Knuth,J.H.Morris和V.R.Pratt三位大佬提出的,看懂之后才能明白这个算法的优美、干练之处

KMP算法的核心是利用匹配失败后的信息,尽量减少模式串str2与主串str1的匹配次数以达到快速匹配的目的

在最后匹配失败后:

在这里插入图片描述

利用匹配失败后的信息可以减少了3次匹配

在这里插入图片描述

这个信息是通过部分匹配表得到的

部分匹配表

  • 前缀:指除了最后一个字符以外,一个字符串的全部头部组合的集合
  • 后缀:指除了第一个字符以外,一个字符串的全部尾部组合的集合
  • 部分匹配值:"前缀"和"后缀"的最长的共有元素的长度

如上面的ABCDABD字符串

  • “A”的前缀、后缀为空集,部分匹配值为0
  • “AB”的前缀为{“A”},后缀为{“B”},部分匹配值为0
  • “ABC”的前缀为{"A","AB"},后缀为{"C","BC"},部分匹配值为0
  • “ABCD”的前缀为{"A","AB","ABC"},后缀为{"D","CD","BCD"},部分匹配值为0
  • “ABCDA”的前缀为{"A","AB","ABC","ABCD"},后缀为{"A","DA","CDA","BCDA"},最长的共有元素为A,长度为1,即部分匹配值为1
  • “ABCDAB”的前缀为{"A","AB","ABC","ABCD","ABCDA"},后缀为{"B","AB","DAB","CDAB","BCDAB"},最长的共有元素为AB,长度为2,即部分匹配值为2
  • “ABCDABD”的前缀为{"A","AB","ABC","ABCD","ABCDA","ABCDAB"},后缀为{"D","BD","ABD","DABD","CDABD","BCDABD"},部分匹配值为0

即可以作部分匹配表:

在这里插入图片描述

部分匹配表的用处

实际上部分匹配表就类型于字符串中重复长度

在这里插入图片描述

当某一位没有匹配到时,查看前一位str2[j-1]的部分匹配值,决定str2移动位数

移动位数 = 已匹配的字符数 - 对应的部分匹配值

在这里插入图片描述

在这个时候,已匹配6个字符,最后一个匹配字符B对应的"部分匹配值"为2,所以应该位移6-2=4

在这里插入图片描述

Java代码实现

实现时关注两个方法:

  • kmpNext(String str2):得到子串str2的部分匹配表
  • kmpSearch(String str1,String str2,int[] next):str1匹配字符串str2
    其中每次比较时,不等就查看部分匹配表来决定位移位数
    相等自然就比较下一位
package com.company.十种算法.kmp;

import java.util.Arrays;

/**
 * Author : zfk
 * Data : 15:47
 */
public class KMP {

    public static void main(String[] args) {

        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";
        int[] next = kmpNext(str2);

        System.out.println("next = "+ Arrays.toString(next));

        System.out.println("index = "+ kmpSearch(str1,str2,next));

    }

    /**
     * kmp搜索
     * @param str1 源字符串
     * @param str2 子串
     * @param next 部分匹配表
     * @return 返回第一个匹配的位置,为-1没有匹配到
     */
    public static int kmpSearch(String str1,String str2,int[] next){

        //遍历
        for (int i = 0,j = 0; i < str1.length();i++){

            //需要处理str1.charAt(i) != str2.charAt(j)
            while (j > 0 && str1.charAt(i) != str2.charAt(j)){
                j = next[j-1];
            }

            if (str1.charAt(i) == str2.charAt(j)){
                j++;
            }
            //子串匹配到了
            if ( j == str2.length()){
                return i - j + 1;
            }
        }

        return -1;

    }


    //获得字符串的部分匹配表
    public static int[] kmpNext(String str2){

        //创建next数组保存部分匹配值
        int[] next = new int[str2.length()];
        //长度为1的字符串部分匹配值为0


        for (int i = 1, j = 0; i < str2.length();i++){

            //当不等时,需要从next[j-1]获得j
            while (j > 0 && str2.charAt(i) != str2.charAt(j)){
                j = next[j-1];
            }

            if (str2.charAt(i) == str2.charAt(j)){
                j++;
            }

            next[i] = j;

        }

        return next;
    }

}

结果:

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值