KMP算法

整篇以主串为 ababcabcacbab ,模式串为 abcac为例。

模式串与主串做匹配时,如果是暴力匹配,在主串某趟匹配失败后,模式串要移动第一位,而主串也有苦难需要回退。

在KMP算法中,如果在匹配过程中,主串不需要回退,当匹配失败后,会从当前位置开始继续匹配。而模式串会滑动到某一位开始比较,而不是没都回退到第一位开始比较。

1. 前缀表:不能不了解

KMP算法中,在写代码之前, 先了解一下KMP算法。首先看一下模式串子串的前后缀。

前缀:除最后一个字符以外,字符串的所有头部子串。后缀:除第一个字符以外,字符串的所有尾部子串。

需要找的是每个子串前缀和后缀相等的最长的前缀和后缀的长度。

求前缀表

abcac 为例:

子串 前缀 后缀 最长相等前后缀长度
a - - 0
ab a b 0
abc ab、a bc、c 0
abca abc、ab、a bca、ca、a 1
abcac abca、abc、ab、a bcba、cac、ac、c 0
下标 0 1 2 3 4
字符串 a b c a c
前缀表 prefix 0 0 0 1 0

所以,字符串 abcac 的最长相等前后端长度是 00010,将这个长度写成数组形式,得到

对应的部分匹配值 [0,0,0,1,0] ,换成另一个名字是,前缀表 prefix = [0,0,0,1,0]

模拟匹配过程

下面,将模式串 abcac 与 主串 ababcabcacbab 进行匹配

第一趟:

主串指针 len = 2 ,模式串指针 i = 2 时,模式串的 c 和主串的 a 匹配失败。已经匹配的字符串是 ab ,查看前缀表,prefix[1] = 0 ,说明 ab 前缀和后缀没有相等的,所以下一趟模式串要回退到第一个字符重新比较,也就是回退到模式串 pattern 的下标为 0 的位置。

下标 0 1 2 3 4 5 6 7 8 9 10 11 12
主串 main a b a b c a b c a c b a b
模式串 pattern a b c

第二趟:

主串指针 len = 6 ,模式串指针 i = 4 时,模式串的 c 和主串的 b 匹配失败。已经匹配的字符串是 abca ,查看前缀表,prefix[3] = 1 ,说明 abca 前缀和后缀有一个字符相等,所以下一趟模式串要回退到第二个字符开始重新比较,也就是回退到模式串 pattern 的下标为 1 的位置。

下标 0 1 2 3 4 5 6 7 8 9 10 11 12
主串 main a b a b c a b c a c b a b
模式串 pattern a b c a c

第三趟:

主串指针 len = 6 ,模式串指针 i = 0 时,模式串的 a 和主串的 b 匹配失败。查看前缀表,prefix[0] = 0 ,说明前缀和后缀没有相等的,因为当前与主串比较的就是模式串的第一个字符,所以,将主串移到下一个位置,与模式串的第一个字符比较。

下标 0 1 2 3 4 5 6 7 8 9 10 11 12
主串 main a b a b c a b c a c b a b
模式串 pattern a b c a c

模式串全部比较完成,匹配成功。整个匹配过程中,主串始终没有回退,所以,KMP算法的时间复杂的是 O(n+m)

某趟发生匹配失败时,如果对应部分的前缀表是0,也就是说已匹配的相等序列中没有相等的前后缀,此时模式串移到第一个字符开始比较。

这个前缀表,似乎和写代码时用的next数组没有关系诶。

2. 前缀表和next数组:是我认识的那个东东

我之前学KMP算法,只知道求next数组,可能书上也有写前缀表,但是从来都是跳过不看,导致我现在才知道前缀表这个东西。然后,才发现,弄懂前缀表才是几年之后再求解next数组时不用翻资料重新学的要点所在。而next数组的公式知识为了实现代码,不是为了我们记住怎么求解next数组的而存在的。毕竟公式那么多字母,那么复杂,不用几年,几个月后就背不出来了。

ababac 为例:

前缀表 prefix = [0,0,0,1,0]

用求next数组的方法求出来的数组是next = [0,1,1,1,2]

这样看起来,prefixnext 的关系,好像并不明显,这么隐晦的吗?

next数组 还有一种表达方式 next = [-1,0,0,0,1] ,这样看来来,prefixnext 好像有点关系了。

将前缀表整体右移一位,然后将空出来的第一位用 -1 填充,就得到了next 数组:

下标 0 1 2 3 4
字符串 a b c a c
前缀表 prefix 0 0 0 1 0
next -1 0 0 0 1

这样,当模式串和主串匹配失败时,直接查看当前匹配失败的字符的前缀表就可以了,而不是查看匹配失败字符前一个字符的前缀表了。

还是以模式串 abcac 与 主串 ababcabcacbab 匹配为例:

当第一趟匹配失败时,主串指针 len = 2 ,模式串指针 i = 2 时,模式串的 c 和主串的 a 匹配失败。查看前缀表,prefix[2] = 0 ,说明 ab 前缀和后缀没有相等的,所以下一趟模式串要回退到第一个字符重新比较,也就是回退到模式串 pattern 的下标为 0 的位置。

下标 0 1 2 3 4 5 6 7 8 9 10 11 12
主串 main a b a b c a b c a c b a b
模式串 pattern a b c

我这里是从0开始存储和计算next数组的,所以此时next数组的含义是:当模式串的第 i 个字符与主串发生匹配失败时,就回退到模式串的 next[i] 重新与主串匹配。有的地方是从1开始存储和计算next数组的。我最后的代码实现中next数组也是从0开始计算的。

可以将next 数组整体 +1 ,所以next数组也可以写成以下形式:

下标 0 1 2 3 4
字符串 a b c a c
next 0 1 1 1 2

3. next数组公式:为了实现代码而已

用于求解next数组的公式如下:

i = 0  ==> next[i] = -1
当最长相等前后缀长度不为0 ==> next[i] = 最大长度
其他情况 ==> next[i] = 0

4. 代码实现:java版

这里的代码中,求解出来的next数组, 下标是从0开始的,数组的第一个值也是 -1。用 java 实现的KMP算法代码如下:

/**
 * KMP 算法
 *
 * @param main    主串,是一个字符串
 * @param pattern 模式串,是一个字符串
 * @return 返回模式串 pattern 与主串 main 匹配时的第一个字符的下标
 */
public static int match(String main, String pattern) {
    int[] next = getNext(pattern);
    int i = 0, j = 0;
    // 模式串与主串匹配
    while (i < main.length() && j < pattern.length()) {
        if (main.charAt(i) == pattern.charAt(j)) {
            i++;
            j++;
        } else {
            if (j == 0) i++;
            else j = next[j];
        }
    }
    return i - pattern.length();
}

/**
 * 求解某个字符串的 next 数组
 *
 * @param pattern 模式串,某个字符串
 * @return 该字符串的 next 数组
 */
public static int[] getNext(String pattern) {
    int len = pattern.length();
    int[] next = new int[len];
    // 求解前缀表prefix
    for (int i = 0; i < len; i++) {
        next[i] = getNextOfx(pattern, next, i);
    }
    return next;
}

/**
 * 求取模式串下标为x的 next数组 的值是多少
 *
 * @param pattern 模式串,某个字符串
 * @param next    next 数组
 * @param x       next数组中的某个下标
 * @return 下标为 x 的 next 数组的值
 */
public static int getNextOfx(String pattern, int[] next, int x) {
    if (x == 0) return -1;
    int len = pattern.length();
    // 找从[0,x-1]的前后缀相等的最大长度
    int i = x - 2;  // 子串前缀的下标,前缀从后向前和后缀比较
    int j = x - 1;  // 子串后缀的下标,后缀从后向前和前缀比较
    int l = x - 1;  // 前缀和后缀相等的最大长度
    while (i >= 0 && j > 0) {
        if (pattern.charAt(i) != pattern.charAt(j)) {
            i--;
            l--;
        } else {
            // 比较该长度下的前缀和后缀是否相等,若相等,则找到前后缀相等的最大长度
            while (i >= 0 && pattern.charAt(i--) == pattern.charAt(j--)) ;
            if (i == -1) return l;
        }
    }
    return 0;
}

你可能感兴趣的:(kmp数据结构)