1.前言
KMP算法又称为模式匹配算法,主要是来求一个长的字符串中是否存在相对应的子串。KMP算法可谓是数据结构串中最难的部分了,我也是花费好长时间翻看大量资料才终于理解,最后写下这篇博客,欢迎大家批评指正,
2.模式匹配的暴力做法
所谓暴力做法就是逐个比较文本串中的每个位置开始的子串与模式串是否匹配,如果不匹配,则直接回退到文本串回退到上一次开始比较的位置的下一个位置。
如下面文本串和模式串不匹配,那么下一步就会回退到开始比较的下一个位置,
即文本串b的位置然后再接着比较,最后一直暴力匹配到最后,此时的时间复杂度会很高。
下面是暴力匹配的代码,
void Violent_match(string &l,string &s)
{
for(int i=0;i<l.size();i++)
{
bool flag=true;
for(int j=0,k=i;j<s.size();j++,k++)
{
if(l[k]!=s[j])
{
flag=false;
break;
}
}
if(flag)
cout<<i<<endl;
}
}
3.KMP算法原理
看了上面的暴力做法,相信各位心中也会想到其实在匹配失败时,有时没有必要直接回溯到原来位置,而是可以回溯到模式串前面最大再可以与文本串匹配的地方。
如下面的例子,此时文本串和模式串不匹配,但是前面已经匹配了一部分,
我们接着利用前面已经匹配的信息,来进行第一步回溯,
此时仍不匹配,则在进行回溯,
然后接着匹配
最后匹配成功
那么我们如何知道当文本串和模式串不匹配时,模式串该回溯到哪个地方,我们这时候就需要用一个数组来存储每次不匹配时该回溯的地方,这就是大名鼎鼎的next数组,也是KMP算法的核心部分。
下面我们先给出KMP模式匹配算法的代码
void KMP(string &l,string &s) //下面ne[]即是next数组
{
for(int i=0,k=-1;i<l.size();i++) //遍历文本串
{
while(k!=-1&&l[i]!=s[k+1])
{
//如果不匹配的话,就根据next数组回溯到上一个位置
k=ne[k];
}
if(l[i]==s[k+1])
{
//如果匹配的话,就看下一个位置是否匹配
k++;
}
if(k+1==s.size())
{
cout<<i-s.size()+2<<endl;//最后输出匹配的子串的位置,从下标1开始输出
k=ne[k];//匹配后回溯,寻找下一个位置
}
}
}
简而言之,KMP算法核心思想在文本串中从左到右逐个比较字符。当发现不匹配时,根据前面已匹配的信息,也就是next数组调整模式串的位置,使模式串向右移动至下一个匹配位置再继续匹配。
4.求next数组
到此处,就是KMP算法最抽象,最难以理解的地方了,很多人就是前面都很明白是怎么回事,到这里就迷糊了,下面我先奉上代码然后再为大家一步一步解释。
void Next(string &s) //下面的ne[]即是next数组
{
ne[0]=-1;//next数组第一个位置不能匹配时无法回溯,故肯定是-1
for(int i=1,k=-1;i<s.size();i++)
{
while(k>-1&&s[k+1]!=s[i])
{
//如果不匹配的话,就进行回溯,看前面有没有可能再度匹配成功
k=ne[k];
}
if(s[k+1]==s[i])
{
//如果匹配的话,还要接着看下一个位置是否匹配
k++;
}
ne[i]=k; //记录回溯位置
}
}
下面我们就来看一个例子:对于模式串abababc,来求next数组。
对于第一个字母a,如果不匹配的话,不用说肯定是没有办法回溯的,故next[0]=-1。
对于第二个字母b,前面没有可以与它可以匹配的,只能回溯到队首,next[1]=-1。
对于第三个字母a,前面第一个字母a与其匹配,故k++,然后接着判断是否下一个字母也匹配,此时next[2]=0;
对于第四个字母b,前面第二个字母b与其匹配,故k++,next[3]=1。
对于第五、六个字母a,b,前面同样可以匹配,故next[4]=2,next[5]=3。
对于最后一个字母c,很明显不能匹配,故需回溯到next[5]=3的位置,这里是字母b仍然与字母c不匹配,此时再回溯到next[3]=1的位置,此时仍不匹配,故最后只能回溯到next[1]=-1,此时可以得到next[6]=-1。
到此我们就可以完整的求出next数组。
5.总结
最后奉上完整代码,供大家参考学习。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1510;
int ne[N];
void Next(string &s) //下面的ne[]即是next数组
{
ne[0]=-1;//next数组第一个位置不能匹配时无法回溯,故肯定是-1
for(int i=1,k=-1;i<s.size();i++)
{
while(k>-1&&s[k+1]!=s[i])
{
//如果不匹配的话,就进行回溯,看前面有没有可能再度匹配成功
k=ne[k];
}
if(s[k+1]==s[i])
{
//如果匹配的话,还要接着看下一个位置是否匹配
k++;
}
ne[i]=k; //记录回溯位置
}
}
void KMP(string &l,string &s)
{
for(int i=0,k=-1;i<l.size();i++) //遍历文本串
{
while(k!=-1&&l[i]!=s[k+1])
{
//如果不匹配的话,就回溯到上一个位置
k=ne[k];
}
if(l[i]==s[k+1])
{
//如果匹配的话,就看下一个位置是否匹配
k++;
}
if(k+1==s.size())
{
cout<<i-s.size()+2<<endl;//最后输出匹配的子串的位置,从下标1开始
k=ne[k];//匹配后回溯,寻找下一个位置
}
}
}
int main()
{
string s1,s2;
cin>>s1>>s2;
Next(s2);
KMP(s1,s2);
for(int i=0;i<s2.size();i++)
{
cout<<ne[i]+1<<" "; //最后打印出next数组每一个值,下标从0开始
}
}
上面代码题目来源为https://www.luogu.com.cn/problem/P3375,大家可以自行练习,有不懂的欢迎大家提问。