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数组如下图所示:

《KMP算法详细讲解,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数组就构造完成了!

结果如下所示:

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

我们再找一个
pattern
试试看:

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

由此来看,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找到第一个子串位置输出,就结束了。具体可根据需求来。

    原文作者:KMP算法
    原文地址: https://blog.csdn.net/you_will_know_me/article/details/77102567
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞