kmp算法记录

看了如何更好地理解和掌握 KMP 算法?之后,做的整理

相关知识

尽管普通模式匹配的时间复杂度是O(mn),KMP 算法的时间复杂度是O(m+n),但在一般情况下,普通模式匹配的实际执行时间近似为O(m +n),因此至今仍被采用。KMP算法仅在主串与子串有很多“部分匹配”时才显得比普通算法快得多,其主要优点是主串不回溯。

什么是前缀,后缀?

以字符串“happy”为例

  • 前缀:h,ha,hap,happ 不包括最后一个字符
  • 后缀:appy,ppy,py,y 不包括第一个字符

部分匹配表(Partial Match Table),简称PMT,将其中的值定义为前缀与后缀的集合交集中最长的元素长度

例如对于:aba:

  • 前缀:a,ab
  • 后缀:ba,a

两者交集的最大长度是a字符,长度为1,则对应的PMT值为1

对于字符串"abababca",PMT表为:

kmp算法记录_第1张图片

再补充上表中“abab”的PMT值:

  • 前缀:a,ab,aba
  • 后缀:bab,ab,b

交集最大长度为ab字符,长度为2,则对应的PMT值为2

如何使用PMT表加速比较?

kmp算法记录_第2张图片

以上图为例:

前边所说的模式字符串 PMT 的性质,主字符串中 i 指针之前的 PMT[j −1] 位就一定与模式字符串的第 0 位至第 PMT[j−1] 位是相同的。

i处发生失配,则s1中的i-j到i与s2中的0到j一定是相同的,在图a中,i和j之前均是ababab。我们可以把s1中阴影部分的abab看作是ababab的后缀,s2中阴影部分的abab看作是ababab的前缀阴影部分相同,所以不需要再去进行比较,从而j直接退回到图b中所在的位置。

阴影部分如何得到? 通过PMT表,PMT[j-1]元素的值就是字符串“ababab”的前后缀交集的最大长度。j-1的原因是,在j处发生失配,s1中的i-j到i与s2中的0到j是相同的,在我看来,这两者相同,PMT表使用起来才有意义。

可以看到如果是在 j 位 失配,那么 j 指针回溯的位置的其实是第 j −1 位的 PMT 值。我们为了编程方便,将PMT数组整体后移一位,空出来0索引的位置,设置为-1,这样做仅仅是为了方便编程!把新数组称为next数组,那么PMT[j-1]==next[j]:

kmp算法记录_第3张图片

KMP算法主体

相比于普通模式匹配算法,kmp的优势在于,主串不回溯。

/**
     * kmp算法
     * @param s1 主串
     * @param s2  字串
     * @return 子串在主串中的位置
     */
public static int kmp(String s1,String s2){
    int i=0,j=0;
    int[] next = getNext(s2);
    while (i < s1.length() && j <s2.length()){
        if(j == -1 || s1.charAt(i) == s2.charAt(j)){  //这里和普通的算法一样,当主串与字串中字符相同时,继续往后遍历
            i++;
            j++;
        }else {  //不同之处,发现不匹配时,j退到next[j]
            j = next[j];
        }
    }
    if( j == s2.length()){
        return i-j;
    }
    return -1;
}

如何求next数组?

kmp算法记录_第4张图片
kmp算法记录_第5张图片

求next数组的过程同样也看成字符串匹配的过程,将上面的s2看成主串,将s2的前缀看成字串。

从主串的第一位(不包括0索引位置)开始对自身进行匹配,能匹配的最大长度就是当前位置的next值。

为什么j=next[j],我的理解是找阴影部分(本文的第二张图)。( j = next[j] == 4 数组的含义,表示在 j 位置字串和主串不匹配的话。j 需要回退到位置为 4 的地方。)

public int[] getNext(String s2) {
        int[] next = new int[s2.length()];
        next[0] = -1;
        int i = 0, j = -1;
        while (i < s2.length() - 1) {
            if (j == -1 || s2.charAt(i) == s2.charAt(j)) {
                i++;
                j++;
                next[i] = j;
            } else {
                j = next[j]; 
            }
        }
        return next;
    }

版本二:

public int strStr(String haystack, String needle) {
    int[] next = getNext(needle);
    // System.out.println(Arrays.toString(next));
    for (int i = 0, j = 0; i < haystack.length();i++ ) {
        while (j > 0 && haystack.charAt(i) != needle.charAt(j)) { //i不轻易++,一直回退
            j = next[j - 1];
        }
        if ( haystack.charAt(i) == needle.charAt(j)) {
            j++;
        }
        if (j == needle.length()) return i - needle.length() + 1;
    }
    return -1;
}

//获取到一个字符串(子串) 的部分匹配值表
public int[] getNext(String s) {
    int[] next = new int[s.length()];
    next[0] = 0;
    for (int i = 1, j = 0; i < s.length(); i++) {
        while (j > 0 && s.charAt(i) != s.charAt(j)) { //这里while和if都可以
            j = next[j - 1]; //回退
        }
        if (s.charAt(i) == s.charAt(j)) {
            j++;
        }
        next[i] = j;
    }
    return next;
}

你可能感兴趣的:(leetcode,算法)