KMP算法详细讲解,next数组构造详解

日常更新。今天我们讲的是KMP算法,先来看道题目:

题目描述

如题,给出两个字符串s1s2,其中s2s1的子串,求出s2s1中所有出现的位置。

输入输出格式

输入格式:

第一行为一个字符串,即为s1(仅包含大写字母)

第二行为一个字符串,即为s2(仅包含大写字母)

输出格式:

若干行,每行包含一个整数,表示s2s1中出现的位置

输入输出样例

输入样例#1

ABABABC

ABA

输出样例#1

1

说明

时空限制:1000ms,128M

数据规模:

s1长度为Ns2长度为M

对于30%的数据:N<=15M<=5

对于70%的数据:N<=10000M<=100

对于100%的数据:N<=1000000M<=1000

       首先拿到这道题目,不知道大家有没有头绪,一般很容易想到的是朴素算法

如下所示:

	cin>>str1;
	cin>>str2;
	int len1=strlen(str1);
	int len2=strlen(str2);
	
	int j=0;
	for(int i=0;i<len1;i++)
	{
		if(str1[i]==str2[j])
		{
			j++;
			if(j==len2)
			{
				cout<<i-j+2<<endl;
				j=0;
			}
		}
		else
		{
			j=0;
		}
	}	

此算法的效率是O(M*N),效率非常的低下,不断地做重复做的事情。

因此有了有了KMP(又叫看毛片算法),KMP算法是一种改进的字符串匹配算法,由D.E.KnuthJ.H.MorrisV.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串匹配次数以达到快速匹配的目的。具体实现就是实现一个next数组,时间复杂度O(M+N)。

    这种算法不太容易理解,最关键的是构造next数组,网上有很多方法,大多讲的晦涩难懂。今天,我用自己的语言最简单的理解方法讲解一下next数组的构造,如果next数组懂了,kmp算法基本就能懂了。

    首先,我们以模式串pattern[] = { "ababaaababaa"}为例,next数组如下图所示:

我们先让pattern[1]=0,pattern[2]=1;至于pattern[0]可以赋值为-1,也有人用它存pattern的长度。

我们从i=3开始,判断pattern[i-1]==pattern[next[j]]是否相等;如果相等,则next[i]=next[j]+1;若不等,则j=next[j],返回继续找,直到相等,就next[i]=next[j]+1,若没有相等,切已经返回到下标j=1;就直接next[i]=1。

                  下面,我们来具体演示一下上图表的构造过程:


当i=3时,j=i-1

         pattern[i-1]=pattern[2]=b;

         pattern[next[j]]=pattern[1]=a;

         因为b!=a且j=2>1

         所以j=next[j]=next[2]=1;

         此时已经来到了第一个元素位置,所以直接置1,next[i]=1,next[3]=1

 

         当i=4时,j=i-1

         pattern[i-1]=pattern[3]=a

         pattern[next[j]]=pattern[1]=a

         因为a=a切j=3>1

         所以next[i]=next[j]+1

                          =next[3]+1

                          =1+1=2

         next[4]=2

 

         当i=5时,j=i-1

         pattern[i-1]=pattern[4]=b

         pattern[next[j]]=pattern[2]=b

         因为b=b切j=4>1

         所以next[i]=next[j]+1

                          =next[4]+1

                          =2+1

                  next[5]=3

 

         当i=6时,j=i-1

         pattern[i-1]=pattern[5]=a

         pattern[next[j]]=pattern[3]=a

         因为a=a切j=5>1

         所以next[i]=next[j]+1

                          =next[5]+1

                  next[6]=3+1=4

 

         当i=7时,j=i-1

         pattern[i-1]=pattern[6]=a

         pattern[next[j]]=pattern[4]=b

         因为a!=b切j=6>1  所以  j=next[j]=next[6]=4

         pattern[next[j]]=pattern[next[4]]=pattern[2]=b

         因为a!=b切j=4>1  所以  j=next[j]=next[4]=2

         pattern[next[j]]=pattern[1]=a

         因为a=a切j=2>1  

         所以next[i]=next[j]+1

                          =next[2]+1

                  next[7]=1+1=2

 

当i=8时,j=i-1

pattern[i-1]=pattern[7]=a

pattern[next[j]]=pattern[2]=b

因为a!=b且j=7>1

所以j=next[j]=next[7]

          j=2

pattern[next[j]]=pattern[1]=b

因为b=b切j=2>1

所以next[i]=next[j]+1

                   =next[2]+1

                   =2

 

当i=9时,j=i-1

pattern[i-1]=pattern[8]=b

pattern[next[j]]=pattern[2]=b

因为b=b且j=8>1

所以next[i]=next[j]+1

                   =next[8]+1

          next[9]=3

 

当i=10时,j=i-1

pattern[i-1]=pattern[9]=a

pattern[next[j]]=pattern[3]=a

因为a=a切j=9>1

所以next[i]=next[j]+1

                   =next[9]+1

          next[10]=3+1=4

 

当i=11时,j=i-1

pattern[i-1]=pattern[10]=b

pattern[next[j]]=pattern[4]=b

因为b=b且j=10>1

所以next[i]=next[j]+1

                   =next[10]+1

          next[11]=5

 

当i=12时,j=i-1

pattern[i-1]=pattern[11]=a

pattern[next[j]]=pattern[5]=a

因为a=a且j=11>1

所以next[i]=next[j]+1

                 =next[11]+1

          next[12]=6

 

下面我们由上面的推理过程用代码实现,我们通过一步一步的演算,不难发现当pattern[i-1]!=pattern[j] && j >1 的时候就要做j=next[j],因此,我们可以用循环代替

		while(s[i-1]!=s[next[j]] && j>1)
			j=next[j];

我们又可以发现,当上述不成立的时候,就要做next[i]=next[j]+1,用代码又可以这又代替

		if(j>1 && s[i-1]==s[next[j]])
			next[i]=next[j]+1;

这样写还有点问题,就是当j=1的时候,需要进行next[i]=1;即:

		if(j>1 && s[i-1]==s[next[j]])
			next[i]=next[j]+1;
		else
			next[i]=1;

我们在套个外循环,因为子串pattern要循环,再加上当初pattern1和2赋值,如下:


pattern[1]=0;
pattern[2]=1;
for(int i=3;i<=strlen(pattern);i++)
{
	int j=i-1;
		while(s[i-1]!=s[next[j]] && j>1)
			j=next[j];
		if(j>1 && s[i-1]==s[next[j]])
			next[i]=next[j]+1;
		else
			next[i]=1;
}

这样next数组就构造完成了!

结果如下所示:




我们再找一个 pattern 试试看:


由此来看,next数组构造完成了。

 

next构造好了,下面就简单了。我们只需要和主串T进行匹配就行了。

定义一个i=1,j=0;如果T[i]!=pattern[j+1] && j>0 也就是当模式串和主串的第一个不相等的时候,就把j=next[j],进行最大匹配移动,依次类推;相反,如果相等,就做++j,让j指向pattern下一个元素和T下一个进行比较;直到j=strlen(pattern)时,就输出i的位置cout<<i-strlen(pattern)+1,如果i==strlen(T)的时候,则就未找到,输出“-1”.

代码如下:

for(int i=1,j=0;i<=strlen(T);i++)
	{
		while(T[i]!=pattern[j+1] && j>0)
			j=next[j];
		if(T[i]==pattern[j+1])
			++j;
		if(j==strlen(pattern))
		{
			cout<<i- strlen(pattern)+1<<endl;
			//j= strlen(pattern);
			break;
		}
		if(i== strlen(T))
		{
			cout<<"-1"<<endl;
		}
	}

到此为止,kmp算法的核心就讲完了。我想只要你理解了我所讲的,kmp算法应该就能掌握了,再找几道例题刷刷,巩固巩固,基本就将kmp算法收入囊中了。

 

下面附上C++的完整代码:

#include"iostream"
#include"string.h"

using namespace std;


void fun(char s[],int next[],int len)
{
	next[1]=0;
	next[2]=1;
	for(int i=3;i<=len;i++) 
	{
		int j=i-1;
		while(s[i-1]!=s[next[j]] && j>1)
			j=next[j];
		if(j>1 && s[i-1]==s[next[j]])
			next[i]=next[j]+1;
		else
			next[i]=1;
	}
	

}

void kmp(char p1[],char p2[],int next[],int len)
{
	for(int i=1,j=0;i<=len;i++)
	{
		while(p1[i]!=p2[j+1] && j>0)
			j=next[j];
		if(p1[i]==p2[j+1])
			++j;
		if(j==next[0])
		{
			cout<<i-next[0]+1<<endl;
			j=next[0];
			break;
		}
		if(i==len)
		{
			cout<<"-1"<<endl;
		}
	}
}
int main()
{
	char p[100];
	char s[100];
	int next[100];
	cin>>p+1;
	int lenp=strlen(p+1);
	
	cin>>s+1;
	int len=strlen(s+1);
	
	next[0]=len;
	
	fun(s,next,len);
//	kmp(p,s,next,lenp);
	
	for(int q=1;q<=len;q++)  cout<<next[q]<<"  ";
	return 0;
}

代码中注释掉的break可以注释掉,注释掉就变成了全查找,把主串中的所有子串中的全部查找出来并输出位置,加个break找到第一个子串位置输出,就结束了。具体可根据需求来。
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。其中,KMP算法的关键在于求解模式串P的next数组。下面详细介绍next数组的含义、求解方法以及应用。 1. 含义 next数组是一个长度为m(m为模式串P的长度)的数组,其中next[i]表示P[0:i]这个子串中,最长的既是其前缀又是其后缀的字符串的长度。特别地,next[0]=-1,next[1]=0。例如,当P="abab"时,其next数组为[-1,0,0,1]。 2. 求解 next数组的求解可以通过动态规划的方式实现。具体来说,在求解next[i]时,假设已知next[0:i-1]的值,我们需要找到一个最长的既是P[0:i-1]的前缀,也是P[1:i]的后缀的字符串。这个字符串可以通过比较P[0:j-1]和P[i-j:i-1]来得到,其中j=next[i-1]+1。 如果P[j]==P[i],那么next[i]=j;否则,我们需要找到一个更短的字符串。此时,我们可以利用next数组的性质,从next[j]开始向前查找,直到找到一个P[k]等于P[i]为止,然后令next[i]=k。如果一直找到k=-1还没有找到,那么next[i]=0。 3. 应用 有了next数组之后,我们就可以利用KMP算法在文本串S中查找模式串P的出现位置。具体来说,我们维护两个指针i和j,分别指向S和P的当前位置。如果P[j]==S[i],那么i和j都向后移动一位;否则,我们利用next数组来决定j的下一步移动位置。具体来说,如果next[j]=-1,或者next[j]<i,则令j=0,i不变;否则,令j=next[j]。这样,我们可以在O(n+m)的时间复杂度内完成匹配。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值