文章目录
- 最长相等前后缀
- next数组
- 概念
- 代码实现
- 图解GetNext中的回溯
- 改进
- 代码实现
- 代码
- 复杂度分析
最长相等前后缀
给出一个字符串 ababa
前缀集合:{a, ab, aba, abab}
后缀集合:{a, ba, aba, baba}
相等前后缀 即上面用同样颜色标识出来的集合元素,最长相等前后缀 也就是所有 相等前后缀 中最长的那一个,也就是上面的 aba
。用图片举例:
最长相等前后缀 就是 KMP
算法滑动的依据。我们用 next
数组存储 最长相等前后缀,以避免每次需要用到 最长相等前后缀 时都需要遍历寻找的繁琐。
next数组
概念
next[i]=j
的含义是:下标 i
之前的字符串其 最长相等前后缀 的长度为 j
。next[0]= -1
(前面没有字符串单独处理)。
a | b | a | b | a | c | d |
---|---|---|---|---|---|---|
next[0] = -1 | next[1] = 0 | next[2] = 0 | next[3] = 1 | next[4] = 2 | next[5] = 3 | next[6] = 0 |
当 s1[5] != s2[5]
时,移动 s2
,让 s2
的前缀(ababa)匹配 s1
的后缀(ababa),即比较 s1[5]
和 s2[next[5]]
。移动的距离是 不匹配位置下标 和 相等前缀 之间的字符数量,即 5-3=2
。
从上面的例子中可以看出,next
的作用有两个:
- 表示该处字符不匹配时应该回溯到的字符的下标。
- 上文提到的:下标
i
之前的字符串其 最长相等前后缀 的长度。
代码实现
class Solution {public:void GetNext(const string& s, vector<int>& next) {int i = 0, j = -1;next[0] = -1; // 下标为0的字符前没有字符串while (i < next.size() - 1) { // 因为函数体中每次先对i++,再对next[i]进行赋值// 因此i需要小于next.size() - 1,以保证自增时不越界if (j == -1 || s[i] == s[j]) {i++;j++; /* 关于 j *//*s[i] == s[j]成立时,next[i] 在 next[i - 1] 的值(j)的基础上 + 1换言之,也就意味着相等前后缀的长度+1,新后缀结尾 i+1 对应的前缀结尾为 j+1*//* j == -1成立时,说明不存在相等前后缀,因此 i 之前的字符串的相等前后缀长度为 next[i] = (-1)++ = 0 */ next[i] = j;}else {j = next[j];// next[j] 是回溯的位置,是 j 指向的字符 之前的字符串的最长相等前后缀的长度// 该操作为了将前缀移动到后缀的位置上,假设 相等长度为 m// 相当于将 (0, j-m)、(1, j-m+1)...(m-1, j-1)匹配上// 举个例子:// 字符串:a b a b a c d// next: -1 0 0 1 2 3// j i// 由于 j 指向的 字符b 其之前的 字符串 aba 最长相等前后缀的长度为 1,// 下标1 作为 新j 就将(0, j-1)匹配上了// 换言之,只需要将 下标1 作为 新j 即可将求 ababac 最长相等前后缀问题转换为// 求 abac 最长相等前后缀的问题。}}}void getNext(const string& pattern, vector<int>& next){ // 另一种写法int i, j = 0;next[0] = -1; //第一个位置不存在数据,为-1for (int i = 1; i < next.size(); i++){//如果当前位置没有匹配前缀,则回溯到求当前后缀的最长可匹配前缀while (j != 0 && pattern[j] != pattern[i]){j = next[j];}//如果该位置匹配,则在next数组在上一个位置的基础上加一if (pattern[j] == pattern[i]){j++;}next[i] = j;}}
};
关于提到的另一种写法,这里不多做分析,可以阅读凌桓大佬的博客。
图解GetNext中的回溯
举个直观的例子:
-
红色部分分别为:最长相等前、后缀。
-
蓝色部分为双指针指向的,待匹配的元素。
-
黑色部分为未开始匹配的部分。
-
绿色部分为
next
数组。
-
如果
s[i] == s[j]
,双指针同时后移,红色区域变大。 -
如果不匹配,必须在 红色部分 重新寻找 相等前后缀,新的相等前后缀长度必然缩短。
紫色部分是红色部分的最长相等前后缀,可以看到,四个紫色部分都是完全相等的。同时,改变 j
的指向,回溯后 j = next[j]
:
- 此时,若
s[i] == s[j]
,又因j
前面的紫色部分和i
前面的紫色部分完全相等。则最长相等前后缀长度+1。 - 不等则进行下一次回溯。图中下一次回溯时不再有相等前后缀,因此不再有紫色部分,不断地回溯,直到
j
指向-1
,此时触发判定条件,执行j++; i++; next[i]=j;
。
改进
举个例子:
- 主串
s = “aaaabaaaaac”
- 子串
t = “aaaac”
当 b
与 c
不匹配时应该 b
与下标为 next[c] = 3
的元素 a
比,这显然是不匹配的,继续回溯,next[next[c]]
回溯后的字符依然是 a
。此时已经 没有必要再将进行回溯了。
节省效率的做法应该是当 b
和 next[3]
不匹配时,就直接回溯到首个 a
的 next
指向(即下标 -1
)。即:
下标 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
元素 | a | a | a | a | c |
next | -1 | 0 | 1 | 2 | 3 |
fail | -1 | -1 | -1 | -1 | 3 |
规则:
- 如果
i
位字符与它next
值指向的j
位字符相等,则该i
位的fail
就指向j
位的fail
值; - 如果不等,则该
i
位的fail
值就是它自己的next
值。
举个其他例子 ababaaab
,进一步体会:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
元素 | a | b | a | b | a | a | a | b |
next | -1 | 0 | 0 | 1 | 2 | 3 | 1 | 1 |
fail | -1 | 0 | -1 | 0 | -1 | 3 | 1 | 0 |
代码实现
这里用 next
表示上面提到的 fail
数组。
void getNext(const string& s, vector<int>& fail) {int i = 1, j = 0;fail[0] = -1; // 下标为0的字符前没有字符串while (i < fail.size()) {if (s[i] != s[j]) { // 字符不匹配fail[i] = j; // 不等时,fail[i] = next[i] = jj = fail[j]; // 回溯}else { // 字符匹配fail[i] = fail[j]; // i 指向的字符与 j 指向字符相等}j++;i++;}
}
输出结果:
代码
class Solution {
public:void GetNext(const string& s, vector<int>& next) {int i = 0, j = -1;next[0] = -1; // 下标为0的字符前没有字符串while (i < next.size()-1) { if (j == -1 || s[i] == s[j]) {i++;j++;next[i] = j;}else {// 如果当前位置没有匹配前缀,则回溯到求当前后缀的最长可匹配前缀j = next[j];}}}void getNext(const string& s, vector<int>& fail) { // 改进的next数组int i = 1, j = 0;fail[0] = -1; // 下标为0的字符前没有字符串while (i < fail.size()) {if (s[i] != s[j]) { // 字符不匹配fail[i] = j; // 不等时,fail[i] = next[i] = jj = fail[j]; // 回溯}else { // 字符匹配fail[i] = fail[j]; // i 指向的字符与 j 指向字符相等}j++;i++;}}int knuthMorrisPratt(const string& query, const string& pattern) {//不满足条件则直接返回falseif (query.empty() || pattern.empty() || query.size() < pattern.size()){return -1;}int i = 0, j = 0;int len1 = query.size(), len2 = pattern.size();vector<int> next(pattern.size(), -1); // next数组GetNext(pattern, next);while (i < len1 && j < len2){if (j == -1 || query[i] == pattern[j]){i++;j++; // i、j各增1}else j = next[j]; // i不变,j回溯}if (j == len2)return(i - len2); // 返回匹配模式串的首字符下标elsereturn -1; // 返回不匹配标志}
};
复杂度分析
- 空间复杂度O(M): 需要借助到一个
next
数组,数组长度为 MMM,MMM 为模式串长度。 - 时间复杂度O(N + M): 时间复杂度主要包含两个部分,
next
数组的构建以及对主串的遍历:next
数组构建的时间复杂度为 O(M);后面匹配中主串不回溯,循环时间复杂度为 O(N),所以KMP
算法的时间复杂度为 O(N + M)。