KMP算法之next数组计算详细解析

KMP算法

  • 相比BF算法的改进:每当一趟匹配过程中出现字符比较不等时,无需回溯i指针(即无需将i指针完全退回至i-j+1),而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的一段距离后,继续进行比较。
  • 需要解决的问题:当主串中的第i个字符与模式中第j个字符比较不相等时,主串中第i个字符(i指针不回溯)应与模式中哪个字符再比较?----假设从主串中第i个字符与模式中的第k个字符再进行比较
  • 它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
      这种信息就是对于每模式串 t 的每个元素 t j,都存在一个实数 k ,使得模式串 t 开头的 k 个字符(t 0 t 1…t k-1)依次与 t j 前面的 k(t j-k t j-k+1…t j-1,这里第一个字符 t j-k 最多从 t 1 开始,所以 k < j)个字符相同。如果这样的 k 有多个,则取最大的一个。模式串 t 中每个位置 j 的字符都有这种信息,采用 next 数组表示,即 next[ j ]=MAX{ k }。

img

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FxNAU0aV-1651734203665)(https://gitee.com/songjie001/typora_pictures/raw/master/2019032020342288.png)]

KMP算法(快速模式匹配算法)C语言详解 (biancheng.net)

充分利用了目标字符串t的性质(比如里面部分字符串的重复性,即使不存在重复字段,在比较时,实现最大的移动量)。

next数组里面存的是什么值呢?其实就是该位置前的字符串前缀与后缀的共用长度(文中的部分匹配值)-1(说白了就是前后相同的长度-1)

next数组计算:

next数组的含义就是一个固定字符串的最长前缀和最长后缀相同的长度。

KMP算法的精髓就在于next数组,从而达到跳跃式匹配的高效模式。

而next数组的值是代表着字符串的前缀与后缀相同的最大长度,(不能包括自身)。

注意:

  • 最长前缀:是说以第一个字符开始,但是不包含最后一个字符。
  • 最长后缀:是说以最后一个字符开始,但是不包含第一个字符。

比如aaaa相同的最长前缀和最长后缀是aaa。

对于目标字符串ptr=ababaca,长度是7,所以next[0],next[1],next[2],next[3],next[4],next[5],next[6]分别计算的是

a:前缀:“”,后缀“”,相同的最长前缀和最长后缀是“”

ab:前缀:a,后缀b,相同的最长前缀和最长后缀是“”

aba:前缀:a、ab,后缀a、ba,相同的最长前缀和最长后缀是a

abab:前缀:a、ab、aba,后缀b、ab、bab,相同的最长前缀和最长后缀是ab

ababa:前缀:a、ab、aba、abab、,后缀a、ba、aba、baba,相同的最长前缀和最长后缀是aba

ababac:前缀:a、ab、aba、abab、ababa,后缀c、ac、bac、abac、babac,相同的最长前缀和最长后缀是“”

ababaca:前缀:a、ab、aba、abab、abab、ababac,后缀a、ca、aca、baca、abaca、babaca,相同的最长前缀和最长后缀是a

img

KMP算法讲解(next数组求解)_Liu Zhian的博客-CSDN博客_kmp算法next

[算法] KMP算法中如何计算next数组 - 简书 (jianshu.com)

next数组计算

注意:

  • 最长前缀:是说以第一个字符开始,但是不包含最后一个字符。
  • 最长后缀:是说以最后一个字符开始,但是不包含第一个字符。
数组下标01234567891011121314
模式串元素agctagcagctagct
next-1000012312345674

n e x t [ j ] = { − 1 j = 0 ( 不存在 t [ 0 ] 到 t [ j − 1 ] 的子串,无意义 ) k , 1 < = k < = j t [ 0 ] 到 t [ j − 1 ] 序列中最长的相同前缀与后缀长度为 k ,即 t [ 0 ] . . . t [ k ] = t [ j − k ] . . . t [ j − 1 ] 0 t [ 0 ] 到 t [ j − 1 ] 序列中不存在相同的前缀与后缀 ) next[j] = \begin{cases} -1 & {\quad j=0(不存在t[0]到t[j-1]的子串,无意义)} \\ k,1<= k <=j & { \quad t[0]到t[j-1]序列中最长的相同前缀与后缀长度为k,即t[0]...t[k]=t[j-k]...t[j-1]} \\ 0 & {\quad t[0]到t[j-1]序列中不存在相同的前缀与后缀)} \\ \end{cases} next[j]= 1k,1<=k<=j0j=0(不存在t[0]t[j1]的子串,无意义)t[0]t[j1]序列中最长的相同前缀与后缀长度为k,即t[0]...t[k]=t[jk]...t[j1]t[0]t[j1]序列中不存在相同的前缀与后缀)

j = 0:不存在t[0]到t[j-1]的子串,无意义,故next[0] = -1(固定)

j = 1:子串t[0]到t[j-1]:a,无前缀与后缀,故next[1] = 0(固定)

j = 2:子串t[0]到t[j-1]:ag,t[0]=a,t[j-1]=t[1]=g,不相等,故next[2] = 0

j = 3:子串t[0]到t[j-1]:agc,t[0]=a,t[j-1]=t[2]=c,不相等,故next[3] = 0

j = 4:子串t[0]到t[j-1]:agct,t[0]=a,t[j-1]=t[3]=t,不相等,故next[4] = 0

j = 5:子串t[0]到t[j-1]:agcta,t[0]=a,t[j-1]=t[4]=a,相等,故next[5] = 1

j = 6:子串t[0]到t[j-1]:agctag,t[1]=g,t[j-1]=t[5]=g,相等,故next[6] = next[5]+1 = 2

j = 7:子串t[0]到t[j-1]:agctagc,t[2]=c,t[j-1]=t[6]=c,相等,故next[7] = next[6]+1 = 3

j = 8:子串t[0]到t[j-1]:agctagca,t[3]=c,t[j-1]=t[7]=a,不相等(一旦出现不相等的情况,则代表不能继续前面计算的相同前缀后缀,要重新找相同的前缀后缀,没有之前的长,按道理需要再从t[0]…与…t[j-1]开始挨个增加对比)

如:此处 t[0]=a,t[j-1]=t[7]=a,相等;故最长的相同前缀后缀为1,next[8] = 1

j = 9:子串t[0]到t[j-1]:agctagcag,t[1]=g,t[j-1]=t[8]=g,相等,故next[9] = next[8]+1 = 2

j = 10:子串t[0]到t[j-1]:agctagcagc,t[2]=c,t[j-1]=t[9]=c,相等,故next[10] = next[9]+1 = 3

j = 11:子串t[0]到t[j-1]:agctagcagct,t[3]=t,t[j-1]=t[10]=t,相等,故next[11] = next[10]+1 = 4

j = 12:子串t[0]到t[j-1]:agctagcagcta,t[4]=a,t[j-1]=t[11]=a,相等,故next[12] = next[11]+1 = 5

j = 13:子串t[0]到t[j-1]:agctagcagctag,t[5]=g,t[j-1]=t[12]=g,相等,故next[13] = next[12]+1 = 6

j = 14:子串t[0]到t[j-1]:agctagcagctagc,t[6]=c,t[j-1]=t[13]=c,相等,故next[14] = next[13]+1 = 7

j = 15:子串t[0]到t[j-1]:agctagcagctagct,t[7]=a,t[j-1]=t[14]=t,不相等(一旦出现不相等的情况,则代表不能继续前面计算的相同前缀后缀,要重新找相同的前缀后缀,没有之前的长,按道理需要再从t[0]…与…t[j-1]开始挨个增加对比)

如:此处 t[0]t[1]t[2]t[3]=agct,t[j-4]t[j-3]t[j-2]t[j-1]=agct,相等;故最长的相同前缀后缀为1,next[14] = 4

此处计算如何合理利用前面已经计算出的next数组?

image-20220504145114759

由前一步的next计算可知,序列 A=B(相同前缀后缀)

新增加t[14]后,计算next[15]时,由于t[7] != t[j-1]=t[14],此时k=7,j=15,故不能继续之前的前缀继续匹配,但也不一定直接退到t[0]处:

假设新增加t[14]后,子串t[0]到t[14]中仍然存在一个长度为k1(k1一定小于k)的序列,t[0]…t[k1]=t[j-k1]…t[14],

此时先不考虑新增的t[14],即先忽略t[14]与t[k1]是否相等,首先要在t[0]到t[k-1]前缀中找出一个与t[j-k+1]…t[13]后缀相等的序列(次长的相同前缀后缀)

若存在次长的相同前缀后缀序列,则可以得出t[0]…t[k1-1]=t[j-k1]…t[13]

image-20220504145537882

借助前面的已知信息或计算出的next:

next[14] = 7, A = B

next[7] = 3, C = D

故 E = C,F = D

故 C = F,忽略t[14]时已有的相同前缀后缀长度为3(next[7]=3,原始k=7,已经比较到t[7],)

此时再度比较t[3]与t[14],若相等,则next[15] = 3 + 1;否则,按照上述方式继续回退,直至退回到比较t[0]与t[j-1]

------> 以上推导,确定是否可能存在次长相同前缀后缀(不为空)的重要条件就是,next[k=7]的值!

------> next[7]为0时,不存在次长相同前缀后缀,直接比较t[0]与t[j];next[k]不为0时,已经存在一个长度为 next[k]的相同前缀后缀序列,下一步继续比较t[k]与t[j]即可。

  • KMP算法的关键就是这个k的回退,灵活运用到已经算出的信息,无需每步回退至0重新匹配判断。

最终next数组的计算代码:

    public static void get_next(char[] t, int[] next) {
        next[0] = -1;//无意义,不存在子串t[0]到t[0-1]
        next[1] = 0;
        int j = 1;
        int k = 0;
        while (j < t.length) {
            //注意k=-1的情况,此时无论字符是否相等,k,j都要加一()
            if (k == -1 || t[k] == t[j]) {
                next[j + 1] = k + 1;
                System.out.printf("next %d = %d\n", j + 1, next[j + 1]);
                k++;
                j++;
            } else {
                System.out.println(next[k]);
                k = next[k];//回退部分,继续比较
                System.out.printf("当j= %d 时,k回退至 %d\n", j + 1, k);
            }
        }
    }

KMF算法与BF算法完整代码:

public class KMF {
    public static void main(String[] args) {
        //System.out.println(Is_KMF("012345678", "4568"));
        char[] t = "agctagcagctagct".toCharArray();
        char[] s = "aaggccagctagctagctagctagcagctagct".toCharArray();
        int[] next = new int[t.length + 1];
        get_next(t, next);
        System.out.println("最终的next数组:");
        for (int i = 0; i < next.length; i++) {
            System.out.printf("next [%d] = %d\n", i, next[i]);
        }
        System.out.println("---------------------------------");
        System.out.println(Is_BF(s, t));
        System.out.println("---------------------------------");
        System.out.println(Is_KMF(s, t, next));
    }

    public static int Is_BF(char[] str, char[] tem) {
        int i = 0;
        int j = 0;
        //当两个串均未比较到串尾
        while (i < str.length && j < tem.length) {
            if (str[i] == tem[j]) {
                i++;
                j++;
            } else {
                i = i - j + 1;
                j = 0;
            }
        }
        if (j == tem.length)
            return i - j;
        else
            return -1;
    }

    public static int Is_KMF(char[] str, char[] tem, int[] next) {
        int i = 0;
        int j = 0;
        //当两个串均未比较到串尾
        while (i < str.length && j < tem.length) {
            if (str[i] == tem[j]) {
                i++;
                j++;
            } else if (j == 0) {
                i++;
            } else {
                j = next[j];
                System.out.printf("j= %d 时,退回至 %d\n", j, next[j]);
            }
        }
        if (j == tem.length)
            return i - j;
        else
            return -1;
    }

    public static void get_next(char[] t, int[] next) {
        next[0] = -1;//无意义,不存在子串t[0]到t[0-1]
        next[1] = 0;
        int j = 1;
        int k = 0;
        while (j < t.length) {
            //注意k=-1的情况,此时无论字符是否相等,k,j都要加一()
            if (k == -1 || t[k] == t[j]) {
                next[j + 1] = k + 1;
                System.out.printf("next %d = %d\n", j + 1, next[j + 1]);
                k++;
                j++;
            } else {
                System.out.println(next[k]);
                k = next[k];//回退部分,继续比较
                System.out.printf("当j= %d 时,k回退至 %d\n", j + 1, k);
            }
        }
    }
}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值