是什么:在主字符串中查找子字符串,返回第一次出现的子字符串的第一个字符索引
为什么:暴力查找字符串的时间复杂度O(n*m),KMP时间复杂度O(n+m)。 n是主串长度,m是模式串长度。
例如待查找字符串ababababca(本文统称为主串s),要匹配的字符串abababca(本文统称模式串p)
按照暴力查找的逻辑,主字符串匹配失败后接着回溯到首字符的下一个位置开始匹配,这样效率是很低的。而KMP可以在匹配失败的时候主串不回溯,只回溯模式串,这样查找速度就快了很多。
部分匹配表,即PMT(Patial Match Table),由最大前缀后缀公共字符串长度组成,例如aba的前缀是a,ab,后缀是a,ba,最大前缀后缀公共字符串是a,长度是1,所以PMT值是1
- 前缀:除最后一个字符的子字符串
- 后缀:除第一个字符串的字符串
因为字符串在匹配失败的时候使用的是上一个位置的next值,故将PMT值右移一位就变成了next数组,第一个位置next值设为-1。
KMP流程:
- 先求next数组,然后匹配,匹配成功则切换到下一个字符,模式串所有字符都匹配成功则算查找成功。
- 匹配失败则获取模式串p当前位置j的next值,将模式串当前位置回溯到next[j],如果又失败了则又回溯,当next值为-1则表示前面j个字符都匹配失败,从主串下一个字符开始匹配。
KMP的代码实现比较简单,关键是要理解算法本身。
这里我有一个疑问:匹配失败的时候主串为什么不需要回溯?怎么证明主串当前位置前n个字符都会匹配失败?关于这一点网上的博客很少有提到,就看到有博客用数学公式证明的。所以这里我从结论倒推,当主串匹配失败时(s[i]和p[j]处),主串s[i]前j个字符必定匹配失败,所以不用回溯。
匹配失败的时候能确定的信息:
- 匹配失败时,模式串当前位置的前j个字符和主串的前j个字符是一样的
- 当next值>=0时,上面的例子next[6]=4,表示ababab前4位和后4位相同,前面6位成功了,所以再匹配前4位也一定会成功,故没有必要再匹配前4位了,下次直接从第4位开始
- 当next值为-1,模式串当前字符前j位都匹配失败了,包括p[j],这时候主串和模式串都右移一位再比较
匹配失败时模式串右移j-next[j]位,即模式串当前位置回溯到p[next[j]]处,代码上就是j=next[j]。
注意:模式串右移只是算法逻辑上的右移,实际代码是没有右移这个操作的,当前位置回溯就相当于右移了。
还有个问题:求next数组的时候最大公共前后缀元素长度为什么要回溯?
这里可以将求next数组看作在模式串中匹配前缀的过程
KMP代码实现:
void get_next(const char *pattern, int *next) {
size_t m = strlen(pattern);
// pattern下标
size_t i = 0;
// 最大公共前后缀元素长度
int j = -1;
next[0] = -1;
while (i < m) {
if (-1 == j || pattern[i] == pattern[j]) {
++i;
++j;
next[i] = j;
}
else {
j = next[j];
}
}
}
int kmp(const char *str, const char *pattern, int *next) {
int n = strlen(str);
int m = strlen(pattern);
if (n < m) {
return -1;
}
// 主串索引
int i = 0;
// 模式串索引,j=-1时表示主串当前字符不匹配并且模式串不能再回溯,然后开始匹配主串下一个字符
int j = 0;
get_next(pattern, next);
while (i < n && j < m) {
if (-1 == j || str[i] == pattern[j]) {
++i;
++j;
}
else {
j = next[j];
}
}
if (j == m) {
return i - j;
}
return -1;
}
#endif
int kmp_test() {
int i;
int next[20] = { 0 };
char *text = "ababababca";
char *pattern = "abababca";
int idx = kmp(text, pattern, next);
printf("match pattern : %d\n", idx);
for (i = 0; i < strlen(pattern); i++) {
printf("%4d", next[i]);
}
printf("\n");
return 0;
}