数据结构与算法Day27----字符串匹配(三):KMP(Knuth Morris Pratt)算法

一、KMP算法:

1、基本原理:

  将不能匹配的字符叫作坏字符,把已经匹配的字符串叫作好前缀。



  当遇到坏字符的时候,把模式串往后滑动,在滑动的过程中,只要模式串和好前缀有上下重合,前面几个字符的比较,就相当于拿好前缀的后缀子串,跟模式串的前缀子串在比较。



  拿好前缀本身,在它的后缀子串中,查找最长的那个可以跟好前缀的前缀子串匹配的。假设最长的可匹配的那部分前缀子串是{v},长度是k。把模式串一次性往后滑动j-k位,相当于每次遇到坏字符的时候,就把j更新为k, i不变,然后继续比较。

  好前缀的所有后缀子串中,最长的可匹配前缀子串的那个后缀子串,叫作最长可匹配后缀子串;对应的前缀子串,叫作最长可匹配前缀子串。



  提前构建一个数组,用来存储模式串中每个前缀(这些前缀都有可能是好前缀)的最长可匹配前缀子串的结尾字符下标。把这个数组定义为next数组,这个数组叫失效函数(failure function)。数组的下标是每个前缀结尾字符下标,数组的值是这个前缀的最长可以匹配前缀子串的结尾字符下标。

2、失效函数计算方法:

<1>、算法思路:

  要计算next[i]的时候,前面的next[0], next[1], ……, next[i-1]应该已经计算出来了。利用已经计算出来的next值,如果next[i-1]=k-1,也就是说,子串b[0, k-1]是b[0, i-1]的最长可匹配前缀子串。如果子串b[0, k-1]的下一个字符b[k],与b[0, i-1]的下一个字符b[i]匹配,那子串b[0,k]就是b[0, i]的最长可匹配前缀子串。所以, next[i]等于k如果b[0, k-1]的下一字符b[k]跟b[0, i-1]的下一个字符b[i]不相等,假设b[0, i]的最长可匹配后缀子串是b[r, i]。如果把最后一个字符去掉,那b[r, i-1]肯定是b[0, i-1]的可匹配后缀子串,但不一定是最长可匹配后缀子串。所以,既然b[0, i-1]最长可匹配后缀子串对应的模式串的前缀子串的下一个字符并不等于b[i],那么就可以考察b[0, i-1]的次长可匹配后缀子串b[x, i-1]对应的可匹配前缀子串b[0, i-1-x]的下一个字符b[i-x]是否等于b[i]。如果等于,那b[x, i]就是b[0, i]的最长可匹配后缀子串。次长可匹配后缀子串肯定被包含在最长可匹配后缀子串中,而最长可匹配后缀子串又对应最长可匹配前缀子串b[0, y]。于是,查找b[0, i-1]的次长可匹配后缀子串,这个问题就变成查找b[0, y]的最长匹配后缀子串的问题了。按照这个思路,可以考察完所有的b[0, i-1]的可匹配后缀子串b[y, i-1],直到找到一个可匹配的后缀子串,它对应的前缀子串的下一个字符等于b[i],那这个b[y,i]就是b[0, i]的最长可匹配后缀子串。

<2>、算法代码:

// b表示模式串, m表示模式串的长度
private static int[] getNexts(char[] b, int m) {
    int[] next = new int[m];
    next[0] = -1;
    int k = -1;
    for (int i = 1; i < m; ++i) {
        while (k != -1 && b[k + 1] != b[i]) {
            k = next[k];
        }
        if (b[k + 1] == b[i]) {
            ++k;
        }
        next[i] = k;
    }
    return next;
}

3、KMP算法:

<1>、算法代码:

// a, b分别是主串和模式串; n, m分别是主串和模式串的长度。
public static int kmp(char[] a, int n, char[] b, int m) {
    int[] next = getNexts(b, m);
    int j = 0;
    for (int i = 0; i < n; ++i) {
        while (j > 0 && a[i] != b[j]) { // 一直找到a[i]和b[j]
            j = next[j - 1] + 1;
        }
        if (a[i] == b[j]) {
            ++j;
        }
        if (j == m) { // 找到匹配模式串的了
            return i - m + 1;
        }
    }
    return -1;
}

// b表示模式串, m表示模式串的长度
private static int[] getNexts(char[] b, int m) {
    int[] next = new int[m];
    next[0] = -1;
    int k = -1;
    for (int i = 1; i < m; ++i) {
        while (k != -1 && b[k + 1] != b[i]) {
            k = next[k];
        }
        if (b[k + 1] == b[i]) {
            ++k;
        }
        next[i] = k;
    }
    return next;
}

<2>、时间、空间复杂度:

(1)、时间复杂度:

  对于构建next数组,i从1开始一直增加到m,而k并不是每次for循环都会增加,所以, k累积增加的值肯定小于m。而while循环里k=next[k],实际上是在减小k的值, k累积都没有增加超过m,所以while循环里面k=next[k]总的执行次数也不可能超过m。因此,next数组计算的时间复杂度是O(m)。
  对于借助next数组匹配:i从0循环增长到n-1, j的增长量不可能超过i,所以肯定小于n。而while循环中的那条语句j=next[j-1]+1,不会让j增长的,那有没有可能让j不变呢?也没有可能。因为next[j-1]的值肯定小于j-1,所以while循环中的这条语句实际上也是在让j的值减少。而j总共增长的量都不会超过n,那减少的量也不可能超过n,所以while循环中的这条语句总的执行次数也不会超过n,所以借助next数组匹配的时间复杂度是O(n)。
  综合两部分的时间复杂度, KMP算法的时间复杂度就是O(m+n)。

(2)、空间复杂度:

  KMP算法只需要一个额外的next数组,数组的大小跟模式串相同。所以空间复杂度是O(m), m表示模式串的长度。

你可能感兴趣的:(数据结构与算法Day27----字符串匹配(三):KMP(Knuth Morris Pratt)算法)