一、tips:
KMP其实本质并不复杂,我尽量用最简单的语句表达;
另外,本人特别喜欢另一种更年轻高效字符串匹配算法——Sunday算法,感兴趣的可以前往查看该参考博文:
https://blog.csdn.net/q547550831/article/details/51860017
二、KMP作用:
字符串匹配。给你两个字符串,寻找其中一个字符串是否包含另一个字符串,如果包含,返回包含的起始位置。
如下面两个字符串:
string str = "bacbababadababacambabacaddababacasdsd";
string ptr = "ababaca";
str 有两处包含 ptr
分别在str的下标10,26处包含ptr。
“bacbababadababacambabacaddababacasdsd”;\
三、KMP步骤详解
1、时间复杂度
KMP算法:可以实现复杂度为O(m+n)
为何简化了时间复杂度:
充分利用了目标字符串ptr的性质(比如里面部分字符串的重复性,即使不存在重复字段,在比较时,实现最大的移动量)。
2、计算next数组
这里我们拟定目标字符串ptr: ababaca
这里我们要计算一个长度为 plen ( ptr 的长度)的转移函数next。
我们首先了解两个概念:
前缀:以第一个字符开始,但是不包含最后的字符
后缀:以最后的字符开始,但是不包含第一个字符
下面是求的过程:(k值理解为 ptr 前k个字符)
所以next数组的值是[-1,-1,0,1,2,-1,0],
这里-1表示不存在,0表示存在长度为1,2表示存在长度为3。这是为了和代码相对应。
注意:由于 next 在C++中是保留字,我用 Next 代替 next 命名
void cal_next(string ptr, int plen)
{
int k = -1; //k初始化为-1
Next[0] = -1; //next[0]初始化为-1,-1表示不存在相同的最大前缀和最大后缀
for (int i = 1; i < plen; i++)
{
while (k > -1 && ptr[k + 1] != ptr[i]) //如果下一个不同,那么k就变成next[k],注意next[k]是小于k的,无论k取任何值。
k = Next[k]; //往前回溯 (请先结合例子验证下,下面还有原理介绍)
if (ptr[k + 1] == ptr[i]) //如果相同,k++
k++;
Next[i] = k; //这个是把算的k的值(就是相同的最大前缀和最大后缀长)赋给next[k]
}
}
3、KMP匹配代码
两个串匹配代码和计算next数组代码很像。不懂为何的不要急,下一个大标题有原理解释。
void KMP(string str, string ptr)
{
int slen = str.length();
int plen = ptr.length();
cal_next(ptr, plen); //计算next数组
int k = -1;
for (int i = 0; i < slen; i++)
{
while (k > -1 && ptr[k + 1] != str[i]) //ptr和str不匹配,且k>-1(表示ptr和str有部分匹配)
{
k = Next[k]; //往前回溯
}
if (ptr[k + 1] == str[i]) //如果相同,k++
k++;
if (k == plen - 1) //说明k移动到ptr的最末端
{
cout << "position: " << i - k << endl;
i = i - k; //i定位到该位置,外层for循环i++可以继续找下一个(这里默认存在两个匹配字符串可以部分重叠
k = -1; //重新初始化,寻找下一个
}
}
}
下面是完整代码
#include<cstdio>
#include<iostream>
#include<string>
#include<algorithm>
#include<cstring>
using namespace std;
int Next[100];
void cal_next(string ptr, int plen)
{
int k = -1; //k初始化为-1
Next[0] = -1; //next[0]初始化为-1,-1表示不存在相同的最大前缀和最大后缀
for (int i = 1; i < plen; i++)
{
while (k > -1 && ptr[k + 1] != ptr[i]) //如果下一个不同,那么k就变成next[k],注意next[k]是小于k的,无论k取任何值。
k = Next[k]; //往前回溯
if (ptr[k + 1] == ptr[i]) //如果相同,k++
k++;
Next[i] = k; //这个是把算的k的值(就是相同的最大前缀和最大后缀长)赋给next[k]
}
}
void KMP(string str, string ptr)
{
int slen = str.length();
int plen = ptr.length();
cal_next(ptr, plen); //计算next数组
int k = -1;
for (int i = 0; i < slen; i++)
{
while (k > -1 && ptr[k + 1] != str[i]) //ptr和str不匹配,且k>-1(表示ptr和str有部分匹配)
{
k = Next[k]; //往前回溯
}
if (ptr[k + 1] == str[i]) //如果相同,k++
k++;
if (k == plen - 1) //说明k移动到ptr的最末端
{
cout << "position: " << i - k << endl;
i = i - k; //i定位到该位置,外层for循环i++可以继续找下一个(这里默认存在两个匹配字符串可以部分重叠
k = -1; //重新初始化,寻找下一个
}
}
}
int main()
{
string str = "bacbababadababacambabacaddababacasdsd";
string ptr = "ababaca";
KMP(str, ptr);
system("pause");
}
三、KMP原理:
相信很多人对 void cal_next(string ptr, int plen) 和 void KMP(string str, string ptr) 中的
语句 k = Next() 不太了解
其实这是KMP的精髓点(NB点~)
原 str 主串为 “bacbababadababacambabacaddababacasdsd”;
我们探究其原理,取红色部分为新 str
string str = "ababada";
string ptr = "ababaca";
下图中
绿色字符 表示每次匹配时第一对不匹配的字符
蓝色背景 表示 str
橙色背景 表示 ptr
i = 0 d, c 不匹配,
按照我们暴力匹配的方法,ptr后移,b,a对齐
i = 1 b, a 不匹配,
ptr后移,a,a对齐
i = 2 d, b 不匹配
ptr后移,b,a对齐
i = 3 d, a 不匹配
ptr后移,a,a对齐
i = 4 d, b 不匹配
......
这个暴力算法推演不知道大家有没有发现什么??,我说一下我的发现:
注意在i = 0中被标红的部分 ababa 长度为5,用 k 表示就是 4 (k = 4)
后面 i = 1到 i = 4 其实都是 str、ptr重叠部分 : ababa 的前后缀在进行匹配!!!
而在 i = 0时其实就是前后缀还未开始匹配,前后缀为空的情况!!!
比如 i = 1时,是 ababa 的后缀 baba 和 前缀 abab 进行匹配(虽然没匹配成功).
但是 i = 2时,前后缀集合的相等的最长串为 aba ,长度为3,即对应 next[k] = 2,即next[4] = 2.
OK!有了这把通往捷径的钥匙 next[4] = 2,我们在以后的匹配中就可以不再暴力匹配了!!!
比如 i = 0时下一对字符 d,c 不匹配,此时我们执行程序 k = next[k] = 2,意思就是承认了 ababa 中有 aba 这个相等前后缀,那么以后在KMP匹配 str,ptr 时就是直接比较 str[i] (即str[5] = ‘d’)与 ptr[k+1] (即ptr[3] = ‘b’) 的字符,跳过了大片区域,而不是像暴力算法从头开始慢慢匹配,如图(注意我现在取的 str = “ababada”,理解的时候看这区域字符串就可以了~):
那么原理其实就是这样~,其他的 k 值及其对应的 next[k] 也是一个道理。
这篇文章就这样啦,觉得不错点个赞呗~
本文参考链接:
https://blog.csdn.net/starstar1992/article/details/54913261
https://www.cnblogs.com/Syhawk/p/4077295.html