KMP 算法及其本质详细讲解

一、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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值