日常更新。今天我们讲的是KMP算法,先来看道题目:
题目描述
如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置。
输入输出格式
输入格式:
第一行为一个字符串,即为s1(仅包含大写字母)
第二行为一个字符串,即为s2(仅包含大写字母)
输出格式:
若干行,每行包含一个整数,表示s2在s1中出现的位置
输入输出样例
输入样例#1:
ABABABC
ABA
输出样例#1:
1
说明
时空限制:1000ms,128M
数据规模:
设s1长度为N,s2长度为M
对于30%的数据:N<=15,M<=5
对于70%的数据:N<=10000,M<=100
对于100%的数据:N<=1000000,M<=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.Knuth,J.H.Morris和V.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找到第一个子串位置输出,就结束了。具体可根据需求来。