KMP算法
- 避免从头匹配:最长相同前缀后缀
- next[]:实现最长相同前缀后缀的思路
- 递推分析:最长相同前缀后缀,从哪里来
- 实现 KMP 算法
避免从头匹配:最长相同前缀后缀
KMP 第一个线性的字符串匹配算法。
算法的优化就是不做无功用,暴力匹配算法每次不匹配时,会重新开始新匹配。
- KMP 的优化在于,知道之前已经匹配的文本,避免从头匹配
那怎么知道之前已经匹配的文本呢?
- 同时满足:最长前缀 = 最长后缀
比如 A A C D A A,最长前缀 AA,最长后缀 AA,匹配过程如下:
- 文本:A A C D A A B B C C D D
- 子串:A A C D A A D
最后一个字母没匹配上,暴力匹配的下一步:
- 文本:A A C D A A B B C C D D
- 子串: A A C D A A D
最后一个字母没匹配上,KMP 算法的下一步:
- 文本:A A C D A A C D A A D
- 子串: A A C D A A D
next[]:实现最长相同前缀后缀的思路
那我们怎么实现最长相同前缀后缀的思路呢?
我们用 next[] 保存串的最长相同前缀后缀。
- next 的作用是,不匹配时,模式串应该从哪里开始重新匹配,避免无用功。
如下图,文本为 t,next为 LPS(最长相同前缀后缀):
第一个字母 A 开始递推,因为前面没有其他字母,所以 LPS[0] = 0
第二个字母 B 和前面的字母 A,没有重合,所以 LPS[1] = 0
第三个字母 A 和前面的字母 A 重合了,所以 LPS[2] = 1
······
第六个字母 B 的最长相同前缀后缀是 A B A B,所以 LPS[6] = 4
递推分析:最长相同前缀后缀,从哪里来
- a:最长相同前缀后缀个数,相同的下标是 t[i-1] == t[a-1]
递推的初始条件:LSP[0] = 0,第一个字母没有前缀,一定等于 0。
假设现在已知第 i-1 个字母最长相同前缀后缀等于 a,即 LPS[i - 2] = a。
请问第 i 个字母的最长相同前缀后缀,是怎么来的?有俩种情况。
- t[i] == t[a],最长相同前缀后缀 + 1
- t[i] != t[a],最长相同前缀后缀不变
情况一,第 i 个字母也相同,最长相同前缀后缀 + 1:
if ( t[i] == t[a] )
LPS[i] = LPS[i-1] + 1 // 或者 LPS[i] = a + 1
情况二:第 i 个字母不同,最长相同前缀后缀不变:
if ( t[i] != t[a] )
LPS[i] = ? // 不相等时,从哪里来?
比如,现在 B 和 C 不相等,LPS[B] = 2,最长相同前缀后缀是:A B。
可怎么让计算机知道呢?
- 最长相同前缀后缀:A B A
- 次最长相同前缀后缀:A B
如果不相等,就需要看次最长相同前缀后缀:
最左边的绿色和橙色块,与中间的绿色和红色块,是否相同。有俩种情况:
- 相同,LPS[i] = 次最长相同前缀后缀(绿色块 + 橙色块)
- 不相同,继续看 — 次次最长相同前缀后缀(绿色块)
我们看图可知,绿色块和红色块不同,但左边绿色块前面没有其他字母了,所以 LPS[i] = 0。
但是,我们需要把这个不断更新 最长相同前缀后缀 - 次次次次最长相同前缀后缀 的过程写出来:
- 绿色部分的长度是 LPS[a-1]
- 最左边绿色部分最后一个字符位置是:LPS[a-1] - 1
- 最左边绿色部分的下一个字符位置是:LPS[a-1]
- 所以,不相等的情况,LPS[i] 从 t[ LPS[a-1] ] 来
a = LPS[i - 1] // 初始值是,最长相同前缀后缀
while ( a > 0 && t[i] != t[a] ) // 不相同时,不断更新
a = LPS[a - 1] // 把 a 替换成次最长相同前缀后缀
if ( t[i] == t[a] ) // 相同时,从前一个来
LPS[i] = LPS[i-1] + 1 // 或者 LPS[i] = a + 1
LPS 实现:
string get_next(string s) {
int n = s.size(); // 文本长度
vector<int> LSP(n, -1); // 记录每个串的最长相同前缀后缀
for (int i = 1; i < n; ++i) { // 遍历每个串
int j = LSP[i - 1]; // 初始化是,最长相同前缀后缀
while (j != -1 && s[j + 1] != s[i]) // 不相同时,不断更新
j = LSP[j]; // 替换为次~次次最长相同前缀后缀
if (s[j + 1] == s[i]) // 相同时
LSP[i] = j + 1; // LSP[i] 从 LSP[i-1] + 1 而来
}
return s.substr(0, LSP[n - 1] + 1); // 避免从头匹配,找到相同子串位置
}
};
实现 KMP 算法
int KMP(char *chang,char *duan) {
int *next = get_next(duan);
int c_strlen = strlen(chang);
int d_strlen = strlen(duan);
int c=0, d=0;
while(c<c_strlen && d<d_strlen){
if( d==-1 || chang[c]==duan[d] ) // 暴力匹配部分,如果相同,俩个指针一起向后一位
c++, d++;
else
d = Next[d]; // 失配,指针回退到对应 Next[]下标,避免从头匹配
}
return d<d_strlen?-1:c-d;
}