kmp算法

witheredwood

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

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

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

1. 前缀表:不能不了解

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

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

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

求前缀表

abcac 为例:

子串前缀后缀最长相等前后缀长度
a--0
abab0
abcab、abc、c0
abcaabc、ab、abca、ca、a1
abcacabca、abc、ab、abcba、cac、ac、c0
下标01234
字符串abcac
前缀表 prefix00010

所以,字符串 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 的位置。

下标0123456789101112
主串 mainababcabcacbab
模式串 patternabc

第二趟:

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

下标0123456789101112
主串 mainababcabcacbab
模式串 pattern abcac

第三趟:

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

下标0123456789101112
主串 mainababcabcacbab
模式串 pattern abcac

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

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

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

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

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

abcac 为例:

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

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

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

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

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

下标01234
字符串abcac
前缀表 prefix00010
next-10001

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

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

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

下标0123456789101112
主串 mainababcabcacbab
模式串 patternabc

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

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

下标01234
字符串abcac
next01112

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 {
            j = next[j];
            if (j == -1){
                j = 0;
                i++;
            }            
        }
    }
    return j == needle.length() ? i - needle.length() : -1;
}

/**
* 求解next数组 [-1, ... , ...]
* 利用java的字符串函数比较前缀和后缀是否相等
*
* @param s 字符串
* @return 该字符串的next数组
*/
private int[] getNext(String s) {
int[] next = new int[s.length()];
next[0] = -1;
// 求next数组
for (int index = 1; index < s.length(); index++) {
  // 当前index字符之前的子串[0, index-1]的相等的最长前后缀
  for (int i = index - 1, j = 1; i > 0 && j < index; i--, j++) {
    String pre = s.substring(0, i);  // 前缀
    String post = s.substring(j, index);  // 后缀
    if (pre.equals(post)) {   // 前缀和后缀相等的最大长度
      next[index] = i;
      break;
    }
  }
}
return next;
}
阅读 970
7 声望
0 粉丝
0 条评论
7 声望
0 粉丝
文章目录
宣传栏