KMP算法的原理和C++实现

前言

最近在实践费曼学习法,将其运用于学习中。kmp算法是数据结构与算法中一个艰难的知识点,弄懂这个算法的原理并使用C++实现耗费了数天时间。写下这篇博客梳理KMP算法的原理和我C++实现算法思路。

简介

KMP算法是基于BF算法的优化算法,BF算法是将主串分解成(主串长度-模式串长度+1)个子串,再一一比较这些子串和模式串,每次比较都要回溯主串的指针,而KMP算法就是在回溯部分进行了优化,主串指针不进行回溯,而是回溯模式串的指针到特定的位置继续匹配,KMP算法的关键部分就是在与寻找模式串回溯的特定位置,这个回溯的特定位置使用数组储存,事实上,这个位置只跟模式串有关,为了说明为什么要回溯到到这个位置,需要引入公共前后缀的概念。

int search_kmp(string& M,string& P,int pos)
{
	int next[P.length()];//next数组储存匹配失败后模式串回溯的位置
	get_next(P,next);//获取模式串指针回溯的位置,这个位置与模式串本身有关
	int i=pos,j=0;//i是主串的指针,j是模式串的指针
	while(j<P.length()&&i<M.length()) 
	{
		if(j==0||M[i]==P[j])
		{
			++i;
			++j;
		}
		else//匹配失败,模式串指针回溯到特定位置
		{
			j=next[j-1];
		}
	}
	if(j==P.length())//匹配成功则j指针指向文本串(主串)的尾元素之后一位 
		return i-P.length();//返回主串中匹配到模式串时的起始位置
	else//匹配失败,返回错误信息,说明没有找到 
		return -1;
}

相关概念

前缀:除去首元素的串的所有子串,例如:串atabat的前缀有t、ta、tab、tatb、tabat。这个串的首元素是a,前缀中不包含首元素a 

后缀:除去尾元素的串的所有子串,例如:串atabat的后缀有a、at、ata、atab、ataba。这个串的尾元素是t,后缀中不包含尾元素t

公共前后缀:相同的前后缀成为公共前后缀,如串atabat的公共前后缀有at。

事实上,next数组储存的值就是公共前后缀的长度,也就是在不匹配时模式串指针回溯到的位置。

KMP算法回溯原理

举例:

为什么可以用公共前后缀来进行回溯呢?这个问题困扰了我很久。

例如:

有主串M,可以分解成由若干子串组成,用ABAC表示(A、B、C是M的子串)

有模式串P,可以分解成分由若干子串组成,用ABAD表示(A、B、D是P的子串)

ABAC

ABAD,两个串在匹配到C、D时发现不一致,则将模式串指针从D回溯到第二个A后面

相当于ABAC

                ABAD

接下来就是匹配主串的子串C与模式串的子串BCD,这就相当于提前完成了部分匹配,不需要主串指针进行回溯,节省了BF算法中主串回溯的步骤,优化了时间复杂度。

上面使用直观的方式进行字符串匹配,在代码中是通过移动模式串指针来达到这样的效果。相当于移动模式串指针到第一个子串A之后,因为模式串数组从0开始,所以这一位置数值上恰好就是公共前后缀A的长度。

事实上,不匹配时公共前后缀长度应是0,利用的是模式串指针值前一位元素的公共前后缀和。

求取公共前后缀长度的值

在代码中,公共前后缀长度的值用next数组储存

void get_next(string& P,int* next)
{
	int j=0;//j是前缀串的尾指针,j是后缀串的尾指针
	next[0]=0;//定义next数组的首元素值为0
	for(int i=1;i<P.length();i++)//后缀的尾指针从1开始 
	{
		//处理前后缀不匹配情况,前后缀不匹配前缀指针回退 
		while(j>0&&P[j]!=P[i])
		{
			//不匹配时j寻找部分匹配的情况,如果存在,则一定是P[0,j-1]的最长公共前后缀 
			j=next[j-1];
		}
		//处理前后缀匹配情况
		if(P[j]==P[i])
		{
			j++;//或者是next[i]=j,j此时也是最长公共前后缀长度 
		} 
		next[i]=j;
	} 
} 

求取next数组需要处理四种情况

1、初始化

2、前后缀相同时

3、前后缀不同时

4、更新next数组的值

初始化

next【0】=0,for循环处理后缀尾指针从1开始,所以需要先处理前缀尾指针为0的情况

前后缀相同时

即推进前缀的尾指针,j++

前后缀不同时

因为后缀指针i向后推进一位,所以后缀变更了。将前后缀都作为一个模式串,寻找这个模式串前一部分与后一部分部分匹配的子串。指针回溯后,进行后一位元素的比较,知道回溯到0或者匹配后为止。

这一部分简书这篇博客讲得很好:

[算法] KMP算法中如何计算next数组 - 简书 (jianshu.com)

更新next的值

我们可以发现,j即前缀的尾指针的值恰好就等于前缀长度(也等于后缀长度),所以可以利用j的值更新next数组

完整代码

#include<iostream>
#include<string>
using namespace std;
void get_next(string& P,int* next);//修改next数组的值 
int search_kmp(string& M,string& P,int pos=0);
void check_next(string& P,int* next);
int main()
{
	string m,pattern;
	cout<<"输入文本串:\n";
	cin>>m;
	cout<<"输入模式串:\n";
	cin>>pattern;
	cout<<"模式串在文本串中的起始位置:\n";
	cout<<search_kmp(m,pattern,0);
	return 0;
}
void get_next(string& P,int* next)
{
	int j=0;//j是前缀串的尾指针,j是后缀串的尾指针
	next[0]=0;//定义next数组的首元素值为0
	for(int i=1;i<P.length();i++)//后缀的尾指针从1开始 
	{
		//处理前后缀不匹配情况,前后缀不匹配前缀指针回退 
		while(j>0&&P[j]!=P[i])
		{
			//不匹配时j寻找部分匹配的情况,如果存在,则一定是P[0,j-1]的最长公共前后缀 
			j=next[j-1];
		}
		//处理前后缀匹配情况
		if(P[j]==P[i])
		{
			j++;//或者是next[i]=j,j此时也是最长公共前后缀长度 
		} 
		next[i]=j;
	} 
} 
int search_kmp(string& M,string& P,int pos=0)
{
	int next[P.length()];
	get_next(P,next);
	int i=pos,j=0;//i是主串的指针,j是模式串的指针
	while(j<P.length()&&i<M.length()) 
	{
		if(j==0||M[i]==P[j])
		{
			++i;
			++j;
		}
		else
		{
			j=next[j-1];
		}
	}
	if(j==P.length())//匹配成功则j指针指向文本串(主串)的尾元素之后一位 
		return i-P.length();
	else//匹配失败,返回错误信息,说明没有找到 
		return -1;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值