KMP算法详解【next数组图解】

KMP算法详解

介绍

 KMP算法是一种改进的字符串匹配算法,其关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。

 在暴力匹配中,每趟匹配失败都是模式后移一位再从头开始比较。而某趟已匹配相等的字符序列是模式的某个前缀,这种频繁的重复比较相当于模式串在不断地进行自我比较,这就是其低效率的根源。
 因此,可以从分析模式本身的结构着手,如果已匹配相等的前缀序列中有某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,主串指针无须回溯,并继续从该位置开始进行比较。而模式向后滑动位数的计算仅与模式本身的结构有关,与主串无关。

  • 字符串的前缀:指除最后一个 字符以外,字符串的所有头部子串;
  • 字符串的后缀:指除第一个字符外,字符串的所有尾部子串;
  • 部分匹配值:字符串的前缀和后缀的最长相等前后缀长度。

例:‘ababa’

  • 'a’的前缀和后缀都为空集,最长相等前后缀长度为0 。
  • 'ab’的前缀为{a},后缀为{b},最长相等前后缀长度为0 。
  • ……
  • 'ababa’的前缀:{a,ab,aba,abab},后缀:{a,ba,aba,baba},最长相等前后缀长度为3。

因此’ababa’的部分匹配值为00123。
image.png

Brute-Force 暴力匹配代码

/**
  *@MethodName bruteForce
  *@Description TODO
  *@Date 2022-07-16 20:01
  *@Param [str1 主串, str2 模式串]
  *@ReturnType int
  */
public static int bruteForce(String str1,String str2){
    //i主串指针,j模式串指针
    int i = 0,j=0;
    while (i < str1.length()&&j < str2.length()) {
        if (str1.charAt(i)==str2.charAt(j)){
            j++;
            i++;
        }else {//不匹配 
            //i回退到本次匹配开始的字符 位置 的后一个字符位置
            i=i-j+1;
            //j会退到0
            j=0;
        }
    }
    if (j>=str2.length()){//说明模式串全部成功匹配了
        return i-str2.length();
    }
    return -1;
}

KMP基本原理

 KMP算法就是在匹配字符串时,利用一个next数组实现,不回退主串指针i 的情况下,确定j回退的位置,来快速匹配。(next数组也就是部分匹配值。next数组的代码实现思路在本章后半部分,这部分的next数组采取人力计算方法。)

 上面提到了,如果已匹配相等的前缀序列中有某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,主串指针无须回溯,并继续从该位置开始进行比较。
其中,**j回退位数=已匹配字符数-对应部分匹配值 **

PS:next数组有两种形式,一种是现在说的部分匹配值,next[0]=0开始的;另一种是将部分匹配值整体右移一个位置,最右侧补-1,即以next[0]=-1开始的。两者本质是一样的。

下面以主串·'ababcabcacbac’模式串’abcac’来展示以下KMP匹配过程:

序号01234
模式串abcac
next00010

 第一次匹配:

 第二次匹配:j回退 已匹配字符数-对应部分匹配值 =2-next[1]=2 位

 第三次匹配:j回退 已匹配字符数-对应部分匹配值 =4-next[3]=4 位

代码实现:

(主要部分,next数组部分在后面)

public static int kmpsearch(String str1, String str2, int[] next) {
    /**
     *@MethodName kmpsearch
     *@Param [str1 源字符串, str2 子串, next 部分匹配位置]
     *@ReturnType int如果是-1没有匹配到,否则返回第一个匹配的位置
     */
    for (int i = 0, j = 0; i < str1.length(); i++) {
        //匹配失败后回退j
        while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
            //j=j- (已匹配字符数-对应部分匹配值)= j-(j-next[j-1])=next[j-1] 
            j = next[j - 1];
        }
        
        if (str1.charAt(i) == str2.charAt(j)) {
            j++;
        }
        //匹配完成后返回匹配字符串位置(第一个字符位置)
        if (j == str2.length()) {
            return i - j + 1;

        }

    }
    return -1;
}

next数组怎么求呢?

 首先,明确 n e x t [ i ] next[i] next[i]的含义:前缀后缀的最大相同字符长度。


 从左边开始扫描模式串,则扫描到第i-1个时next[i-1]的状态如下图所示:

 那么可以计算next[i]:

  • 如果模式串dest[i]==dest[ next[i-1] ] (即dest[n]),那么很明显next[i]=next[i-1]+1 =n+1
  • 如果不相等,那么我们要对dest[n-1]的字符有所期待,万一 第0~n-1个字符和后缀相同呢?如下:

 对于前0~n-1的字符同样存在m长度的相同前后缀,即next[n-1]=m。

  • 那么如果dest[i]==dest[n-1] 也就是dest[i]==dest[ next[n-1] ],那么很明显next[i]=next[n-1]+1 =m+1。
  • 如果不相等,那么我们继续对dest[m-1]的字符有所期待。

 这和上面的步骤是不是一样的?这样就可以依次循环下去直至找到相同字符,或者直至前面没有字符为止。

在这里插入图片描述

代码实现:

上述过程转换成代码:

/**
 *@MethodName kmpnext
 *@Description TODO 获取一个字符串(子串)的部分匹配值
 *@Date 2022-07-16 22:28
 *@Param [dest]
 *@ReturnType int[]
 */
public static int[] kmpnext(String dest) {
    int[] next = new int[dest.length()];
    next[0] = 0;
    for (int i = 1, j = 0; i < dest.length(); i++) {
        //当dest.charAt(i) != dest.charAt(j),我们需要从next[j-1]获取新的j
        //直到我们发现有dest.charAt(i) == dest.charAt(j)成立才退出
        while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
            j = next[j - 1];
        }
        //当满足dest.charAt(i) == dest.charAt(j)时,部分匹配值+1
        if (dest.charAt(i) == dest.charAt(j)) {
            j++;
        }
        next[i] = j;
    }
    return next;
}

整体代码

public static int kmpsearch(String str1, String str2, int[] next) {
    /**
     *@MethodName kmpsearch
     *@Param [str1 源字符串, str2 子串, next 部分匹配位置]
     *@ReturnType int如果是-1没有匹配到,否则返回第一个匹配的位置
     */
    for (int i = 0, j = 0; i < str1.length(); i++) {
        //匹配失败后回退j
        while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
            //j=j- (已匹配字符数-对应部分匹配值)= j-(j-next[j-1])=next[j-1] 
            j = next[j - 1];
        }
        
        if (str1.charAt(i) == str2.charAt(j)) {
            j++;
        }
        //匹配完成后返回匹配字符串位置(第一个字符位置)
        if (j == str2.length()) {
            return i - j + 1;

        }

    }
    return -1;
}

/**
 *@MethodName kmpnext
 *@Description TODO 获取一个字符串(子串)的部分匹配值
 *@Date 2022-07-16 22:28
 *@Param [dest]
 *@ReturnType int[]
 */
public static int[] kmpnext(String dest) {
    int[] next = new int[dest.length()];
    next[0] = 0;
    for (int i = 1, j = 0; i < dest.length(); i++) {
        //当dest.charAt(i) != dest.charAt(j),我们需要从next[j-1]获取新的j
        //直到我们发现有dest.charAt(i) == dest.charAt(j)成立才退出
        while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
            j = next[j - 1];
        }
        //当满足dest.charAt(i) == dest.charAt(j)时,部分匹配值+1
        if (dest.charAt(i) == dest.charAt(j)) {
            j++;
        }
        next[i] = j;
    }
    return next;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

SS上善

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

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

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

打赏作者

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

抵扣说明:

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

余额充值