1. KMP算法目的
当文本串 S 匹配到 i 位置,模式串 P 匹配到 j 位置匹配失败时, i 不动,j 回退,但是根据已知信息尽可能少的回退
2. next数组中记录的到底是什么
next数组记录匹配失败时,模式串的 j 回退位置
3. 计算next数组时,为什么要找最长相同前后缀
文本串s a b c a b a
模式串p a b c a b c
(1)模式串是怎么回退的
当前 a 与 c 不相同,我们希望从文本串的 a 向前 (<----) 找一个子串,它与模板串从下标 0 开始往后 (---->) 的一个字串相同,这个字符串的长度为 n,那么模式串的j就回退到 n
就上面这个例子,绿色背景部分就是相同的字串部分,长度为2,j 就回退到下标 2 也就是 c 进一步进行匹配(为啥回退下标等于相同字串的长度,因为模式串下标从 0 开始)
(2)回退与后缀的关系
当文本串匹配到 a 的时候才失败说明,之前匹配是正确的,那么对应模板串的紫色部分跟文本串的绿色部分相同,进而跟模式串的绿色的部分是相同的。那么回退问题就可以与文本串进行脱离,转化为模式串最长前后缀的问题
(3)手工计算 next 数组注意啥
既然 next 数组时当 j 这个位置匹配失败后退到哪,那么 next 数组肯定跟 j 没有关系。要看以下标 0 的字符开头,以下标 j - 1 的字符为结尾的不包括自身的最长相同串
4. 如何计算next数组
(1)规定 next[0] = -1; next[1] = 0; 如果第一个字符就匹配不上,无路可退, 第二个个字符匹配不上回到开头
(2)在已知 next[ j ] = k 的前提下如何 计算next[ j + 1 ]
next[ j ] = k 表示当下标 j 不匹配时回退到下标k 也就是有一个长度为 k 的 相同前后缀字串
p[ 0 ]……p[ k - 1] == p[ j - k] …… p[ j -1]
(因为子串长度为k,那么后缀字串起始位置为j - 1 - k +1 = j - k)
第一种情况 p [ k ] == p[ j ] 就在之前基础上又加了一个相等
p[ 0 ]……p[ k - 1] p[ k ] == p[ j - k] …… p[ j -1] p[ j ]
此时 next[ j + 1] = next[ j ] + 1 = k + 1
第二种情况 p [ k ] != p[ j ] 那么 j = k k = next[ j ] 一直回退 直到 p[ j ] == p[ k ]
next[ j + 1 ] = k + 1
(3) 为什么要回退,为什么回退就是可以找到想要的值
第一种解释(我个人的解释):现在是找相同前后缀,实质上还是字符串匹配
假定
0 1 2 3 4 5 6 7
a b c a b a b c
-1 0 0 0 1 2
计算next[ 6 ] next[ 5 ] = 2 a ! = c 我们转为下面这种字符串匹配的形式,
a b c a b a b c
a b c a b a b c
跟文本串不相等,模板串那就回退吧 j = next[ j ]。
要么找到模块串回退到的那个字符跟文本串相等,又符合
p[ 0 ]……p[ k - 1] p[ k ] == p[ j - k] …… p[ j -1] p[ j ] next[ j + 1 ] = k + 1
要么就模板串回退到 -1 , -1+1 = 0 还是 next[ j + 1 ] = k + 1
第二种解释:看起来就很对,但是思想我概括不了
假定我们要计算next[ 16 ] 此时 next[15] = 7
也就是 p[ 0 ] …… p[6] == p[ 8 ]……p[ 14 ] 前后两个部分是重合的。
如果 p[15] == p[7] next[16] = 7 + 1 = 8
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
7 |
如果 p[7] != p[15]
假定 next[7] = 3
p[ 0 ] …… p[2] == p[ 4 ]……p[ 6 ] == p[ 8 ] …… p[10] == p[ 12 ]……p[ 14 ] 这四个部分是重合的
重点是首尾两块是相等的 此时如果p[15] ==p[3] 那么next[16] = 3 + 1 = 4
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
3 | 7 |
如果p[3] != p[15]
继续回退 next[3] = 1 比较p[15] 与p[1] 。一直回退,直到找到相同或者是下标为-1
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
1 | 3 | 7 |
KMP优化
先计算next数组,再遍历计算next_val数组
避免 aaaaaaaaaaaaaaaaaaab 一个个回退的问题
class Solution {
public:
int strStr(string haystack, string needle) {
int n = haystack.size();
int m = needle.size();
if(n < m) return -1;
int next[m];
getNext(next, needle);
int i=0;
int j=0;
while(i < n && j < m){
if(j == -1 || haystack[i] == needle[j]){
i++;
j++;
}else{
j = next[j];
}
}
if(j >= m) {
return i-j;
}
return -1;
}
void getNext(int *next, string &needle){
next[0] = -1;
if(needle.size()==1) return;
next[1] = 0;
int k = 0;
for(int i = 2; i< needle.size(); i++){
while(k != -1 && needle[k] != needle[i-1]){
k = next[k];
}
next[i] = k+1;
k = next[i];
}
}
};
以abababab为例 ,ab就是最小重复单位, 最长相等前后缀不包含的子串 ababab 少了的那个就是最小
数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
之前求最长前后缀的时候,没有带上最后一个元素的,这里需要判断一下
class Solution {
public:
bool repeatedSubstringPattern(string s) {
int n = s.size();
if(n <= 1) return false;
int next[n];
next[0] = -1;
next[1] = 0;
int k = 0;
for(int i=2; i<n; i++){
while(k!=-1 && s[i-1] != s[k]) k = next[k];
next[i] = k + 1;
k = next[i];
}
if(s[next[n-1]]!=s[n-1]) return false;
if(n % (n - (next[n-1]+1)) == 0) return true;
return false;
}
};