kmp算法详解

试想现在有这样一个问题,有一个文本串S,和一个模式串P,求P在S中的位置,需要怎么求呢?
若用暴力的方法,时间复杂度为O(n*m),当字符串长度过大时显然不行。
现在介绍另一种方法,可以将复杂度降到O(n + m)级别,这就是kmp。
———————————————————————————————————————————————————————

20190826更新:

此部分可以看完后面部分再看。
关于next数组,之前以为对代码懂了,也能模拟出求解过程,但一直感觉对kmp的理解还不够彻底,再后来,嗯,有点柳暗花明的感觉了。

再回顾一下next数组,
nex数组的值既代表当前字符之前的子串最长相同的前缀后缀值,也代表前缀的下一位字符下标。
个人感觉,nex【0】= -1,这个赋值是nex数组的精髓所在。当运行到k = -1时,代表下一位之前的子串没有相容前后缀,会直接将其nex值赋为0。
算法是不断地将前缀下标k与后缀下标j所代表的字符进行比较,若相同,令j++,k++。此时j代表后缀的下一位,k既代表前缀的下一位的下标,也为相同前后缀长度,赋为nex【j】 = k。若不同,寻找nex【k】,再次比较。
1.为什么是j++, k++?
因为求解nex数组时是不断地令j, k所代表的字符进行比较,但赋值是赋值给后缀j的下一位,所赋的值是前缀k的下一位的坐标,即k + 1。k, j分别自增一位刚好满足。
2.为什么要把nex【0】赋为-1:?
参考1,若要把某一位的nex值赋为0, 在赋值之前会进行k++这个操作,此时k应为-1。k = -1代表着此时以j所代表字符为结尾的字符串没有相容的前后缀。并且,k = 0时会将p【0】与p【j】进行比较,来判断将下一位是赋值为1还是再进行一次循环赋值为0。

再强调一点,kmp()这个函数里的k < plen && j < slen不能换成k < p.size() && j < s.size()。因为str.size()返回的值无符号数,而k与是有符号数,并且可能等于-1,在比较时会把有符号数转换为无符号数,此时会出现-1>str.size()的情况,进而直接跳出循环。因此这种写法是错误的。
————————————————————我是分界线 ——————————————————————————————

在讲解kmp之前,先介绍一下next数组,next数组可以说是kmp的精髓所在。

1.next数组

next数组里面的值的含义:
1.代表当前字符之前字符中有多大长度的相同前缀和后缀。
2.代表着此字符之前最大相同长度前缀的下一个字符的下标。

在这里插入图片描述
先看代码:

void getnext()
{
	int len = p.size();
	nex[0] = -1;
	int k = -1, j = 0;//k 为前缀下标,j为后缀下标 
	while(j < len - 1)
	{
		if(k == -1 || p[j] == p[k])
		{
			j++, k++;
			nex[j] = k;
		}
		else
		{
			k = nex[k];
		}
	}
}

next数组求解时可以看做自身不断与自身进行比较。
接下来我们模拟一下求解next数组的过程。
在这里插入图片描述
以上面这个模式串为例,刚开始时前缀下标k为-1,执行if语句,把下标为1的字符值(b)赋值为1,此时k为0, j为1。
在这里插入图片描述
然后p【0】和p【1】进行比较,两者不等,执行k = nex【k】,此时k = -1,再次循环,赋值nex【2】 = 0。直至进行至下图位置。
在这里插入图片描述
此时k = 0, j = 3,两者相等,执行if语句,j++, k++,赋值nex【4】 = 1。
之后比较p【1】和p【4】两者相等,赋值nex【5】 = 2。
再次循环,比较p【2】和p【5】,两者不等,执行k = nex【2】, 为0,即让p【0】与p【5】进行比较,此时程序进行到下图的位置。
在这里插入图片描述
还不相等,再次执行k = nex【k】,此时k值为-1,再次循环,将k【6】赋值为0.
至此,nex数组求解完成。
在这里插入图片描述
此时,不难发现,next数组的求解过程是不断让自身与自身比较,当发现前缀k所代表的字符和后缀j所代表的字符相同时,令下一位的next值赋为k + 1,若不同,令后缀j所代表的字符与next【k】所代表的字符进行比较,直至与p【0】比较。若与p【0】也不同,就令k = -1, 下一次循环式直接让下一位赋值为0。

下面,我们来看看具体的kmp算法。

2.kmp

先看代码:

int kmp()
{
	int slen = s.size(), plen = p.size();//s为文本串,p为模式串 
	int i = 0, j = 0;
	while(i < slen && j < plen)//不能写为 k < p.size() && j < s.size()
	{
		if(j == -1 || s[i] == p[j])
		{
			i++, j++;
		}
		else
		{
			j = nex[j];
		}
	}
	if(j == plen)
		return i - j;
	else
		return -1;
}

下面,来模拟一下kmp算法的具体过程
在这里插入图片描述
先令s【0】与p【0】比较,相等,然后i++,j++, 比较下一位,直至比较至i = 5, j = 5的位置。
在这里插入图片描述
此时发现p【5】与s【5】不等,令j = nex【j】,即赋值j = 2,将p【2】与s【5】进行比较,等价于p数组右移3位,不等。在这里插入图片描述
再次执行j = nex【j】,赋值j = 0, 令s【5】与p【0】进行比较,仍然不等,赋值j = -1,循环,进入if语句,再次循环令s【6】与p【0】进行比较。

在这里插入图片描述
相等,继续比较下一位,直至i == slen || j = plen,结束循环。
此时,可以发现,文本串s只遍历了一遍,nex数组确定了匹配失败后下一次应该匹配的位置,而不是返回文本串s的下一位。

但是,nex数组还能再优化些。

3.next数组的优化

看下面这个例子:
在这里插入图片描述
匹配s串与p串直至p【5】的位置,不等,令j = nex【j】,即赋值j = 2,令p【2】与
s【5】进行比较,仍然不等。
在这里插入图片描述
但仔细想想,在匹配中,我们已经知道p【5】= e,失配,而执行j = nex【5】之后,令p【nex【5】】 = p【2】 = e再跟s【5】匹配,必然失配,那么问题出现在哪?
问题出现在p【j】 = p【nex【j】】。当p【j】!= s【i】时,下一次必然是
p【nex【j】】与 s【i】进行匹配,若p【j】 = p【nex【j】】,下一次匹配必然失败。所以,不能出现p【j】= p【nex【j】】的情况。那怎么优化呢?我们只需要在出现p【j】= p【nex【j】】时,令nex【j】 = nex【nex【j】】就好了。见下面的代码:

void getnext()
{
	int len = p.size();
	nex[0] = -1;
	int k = -1, j = 0;//k 为前缀下标,j为后缀下标 
	while(j < len - 1)
	{
		if(k == -1 || p[j] == p[k])
		{
			j++, k++;
			//主要改动在下面四行 
			if(p[j] != p[k])
				nex[j] = k;
			else
				nex[j] = nex[k];
		}
		else
		{
			k = nex[k];
		}
	}
}

至此,kmp算法讲解结束!
最后,附上完整的kmp代码:

#include <iostream>
#include <cstring>
using namespace std;

const int maxn = 1e4 + 10;
string s, p;
int nex[maxn];

int kmp()
{
	int slen = s.size(), plen = p.size();//s为文本串,p为模式串 
	int i = 0, j = 0;
	while(i < slen && j < plen)//不能写为 k < p.size() && j < s.size()
	{
		if(j == -1 || s[i] == p[j])
		{
			i++, j++;
		}
		else
		{
			j = nex[j];
		}
	}
	if(j == plen)
		return i - j;
	else
		return -1;
}

void getnext()
{
	int len = p.size();
	nex[0] = -1;
	int k = -1, j = 0;//k 为前缀下标,j为后缀下标 
	while(j < len - 1)
	{
		if(k == -1 || p[j] == p[k])
		{
			j++, k++;
			//主要改动在下面四行 
			if(p[j] != p[k])
				nex[j] = k;
			else
				nex[j] = nex[k];
		}
		else
		{
			k = nex[k];
		}
	}
}

int main()
{
	cin >> s >> p;
	getnext();
	cout << kmp();
	return 0;
}
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值