整篇以主串为 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 |
字符串 | 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 的位置。
主串 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 的位置。
主串 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
,说明前缀和后缀没有相等的,因为当前与主串比较的就是模式串的第一个字符,所以,将主串移到下一个位置,与模式串的第一个字符比较。
主串 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数组的而存在的。毕竟公式那么多字母,那么复杂,不用几年,几个月后就背不出来了。
以 abcac
为例:
前缀表 prefix = [0,0,0,1,0]
,
用求next数组的方法求出来的数组是 next = [0,1,1,1,2]
,
这样看起来,prefix
和 next
的关系,好像并不明显,这么隐晦的吗?
next数组 还有一种表达方式 next = [-1,0,0,0,1]
,这样看来来,prefix
和 next
好像有点关系了。
将前缀表整体右移一位,然后将空出来的第一位用 -1
填充,就得到了next 数组:
字符串 | 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 的位置。
主串 main | a | b | a | b | c | a | b | c | a | c | b | a | b |
模式串 pattern | a | b | c |
我这里是从-1开始存储和计算next数组的,所以此时next数组的含义是:当模式串的第 i
个字符与主串发生匹配失败时,就回退到模式串的 next[i]
重新与主串匹配。有的地方是从0开始存储和计算next数组的。我后面的代码实现中next数组也是从-1开始计算的。
可以将next 数组整体 +1
,所以next数组也可以写成以下形式:
字符串 | 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;
}