目录
在讲解KMP算法之前,先说一说BF算法。
BF算法
BF算法是一种暴力的字符串匹配算法,假设主串是s,用i去维护匹配的位置,模式串是sub,用j去维护匹配的位置。
i、j均从字符串开头开始匹配,每次匹配失败后,i回退到s开始匹配的位置的后一个位置,j又从sub开头开始匹配。
M*(N-M+1),O(M*N)
KMP算法
KMP算法是一种改进的字符串匹配算法。它的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
KMP和BF不一样的地方在:主串的i并不会回退,j也不会每次直接移动到0号位置。
首先举例,为什么主串不回退?
假设目前在2号位置匹配失败,就算让i按BF算法的规则回退到1位置,也是不必要的,因为b与子串0位置的a也不一样。
j回退的位置
此时匹配失败,我们不进行回退i,因为在这个地方匹配失败,说明i的前面和j的前面是有一部分相同的,不然两个下标不可能走到这里。
我们发现如果j回退到2下标而i不回退,这就是最好的情况了,问题就是怎么知道j回退到2下标?
next数组
KMP的精髓就是next数组,也就是用next[j]=k来表示不同的j对应的一个k值,这个k就是将来j位置不匹配时j回退到的位置。
求k的规则:
1、找到两个相等的真子串(不能是同一个,但可以部分重合),一个以下标0开始,一个以j-1下标结束,k值就是真子串的长度。
2、找不到,next[j]=0。
3、不管什么数据,next[0]=-1,next[1]=0
(如果有next[0]=0,next[1]=1,那么把我们按这个规则求得的next数组统一+1即可)
首先两个串能匹配到黑线位置处,说明模式串前面都与主串匹配上了,所以绿线部分一定是相同的,然后我们再按求k的规则,假设存在,那么我们就可以找到蓝线部分的字符串与绿线相同,所以最好的情况就是i不回退,将j回退到红线位置,继续去比较红线处的字符与主串黑线处的字符。k值是真子串的长度的原因是蓝串从下标0开始,那么红线字符的下标就是真子串的长度。
求next数组的练习
我们可以发现,next数组增大时,只能+1;不存在两个相同的子串时,不一定回退到0,下面证明:
已知next[i]=k,怎么求next[i+1]?
因为next[i]=k,那么就有s0...sk-1==sx...si-1;由于k-1-0==i-1-x,所以x==i-k,
所以s0...sk-1==si-k...si-1,
如果sk==si,那么s0...sk==si-k...si,next[i+1]=k+1。
如果sk!=si,
那么k要继续回退,直至sk==si,或者k越界,回退到0位置,此时k=0,next[i+1]=k+1=1。
void Getnext(vector<int>& next,string& sub){
next[0]=-1;
if(sub.size()==1) return;
next[1]=0;
int i=1,k=next[1];
//不能用for循环i++,因为在k往前找时,i不应该动
while(i<sub.size()){
//k有可能等于-1,代表没有两个完全相同的子串,此时next[i]=0
if(k==-1||sub[i]==sub[k]){
i++;
k++;
next[i]=k;
}
else{
k=next[k];
}
}
}
//从主串s的pos位置开始查找sub
int KMP(string& s,string& sub,int pos){
if(s.empty()||sub.empty()||s.size()<sub.size()) return -1;
//i控制主串的匹配的位置,不能用for循环i++,因为在匹配不上的时候i是不动的
int i=pos,j=0;
vector<int>next(sub.size()+1);
Getnext(next,sub);
while(i<s.size()&&(j==-1||j<sub.size())){
//next[0]=-1,j有可能等于-1,表示没有能和s[i]匹配上的,也需要进入
//不能只写j<sub.size()是因为sub.size()返回的是一个无符号数
if(j==-1||s[i]==sub[j]){
i++;
j++;
}
else{
j=next[j];
}
}
if(j>=sub.size()) return i-j;
return -1;
}
next数组的优化
如这个例子, 如果主串是aaaaaaaac,当b与c不匹配的时候,模式串的j按照next数组会从b回退到7号位a,c与a仍然不匹配,j继续回退到6号位a,以此类推最终到-1。因为回溯后的字符与原字符相同,原字符不匹配,回溯后的字符自然也不匹配,所以我们可以对上述情况进行优化。
规则:
回退到的字符与当前位置的字符相同,当前位置的nextval就写回退到的那个字符的nextval。
s[i]==s[next[i]],nextval[i]=nextval[next[i]]
回退到的字符与当前位置的字符不相同,当前位置的nextval就写当前位置的字符的next。
s[i]!=s[next[i]],nextval[i]=next[i]
void Getnextval(string& sub,vector<int>& nextval){
nextval[0]=-1;
if(sub.size()==1) return;
int i=0,k=nextval[0];
while(i<sub.size()){
if(k==-1||sub[i]==sub[k]){
//本来是next[i]=k+1
i++;
k++;
if(sub[k]==sub[i]){
nextval[i]=nextval[k];
}
else{
nextval[i]=k;
}
}
else{
k=nextval[k];
}
}
}
int KMP(string& s,string& sub,int pos){
if(s.empty()||sub.empty()||s.size()<sub.size()) return -1;
int i=pos,j=0;
vector<int>nextval(sub.size()+1);
Getnextval(sub,nextval);
while(i<s.size()&&(j==-1||j<sub.size())){
if(j==-1||s[i]==sub[j]){
i++;
j++;
}
else{
j=nextval[j];
}
}
if(j>=sub.size()) return i-j;
return -1;
}
例题:
训练赛20190304 - Virtual Judge (vjudge.net)
一行多次匹配(可重叠)
#include<iostream>
#include<vector>
#include<string>
using namespace std;
string sub,s;
void Getnextval(vector<int>& nextval){
nextval[0]=-1;
if(sub.size()==1) return;
int i=0,k=nextval[0];
while(i<sub.size()){
if(k==-1||sub[i]==sub[k]){
i++;
k++;
if(sub[i]==sub[k]){
nextval[i]=nextval[k];
}
else{
nextval[i]=k;
}
}
else{
k=nextval[k];
}
}
}
int KMP(){
int num=0;
if(sub.empty()||s.empty()||s.size()<sub.size()) return num;
vector<int>nextval(sub.size()+1);
Getnextval(nextval);
int i=0,j=0;
while(i<s.size()){
if(j==-1||s[i]==sub[j]){
i++;
j++;
}
else{
j=nextval[j];
}
if(j==sub.size()){
num++;
j=nextval[j];//可重叠
//j=0;//不可重叠
}
}
return num;
}
void solve(){
cin>>sub>>s;
cout<<KMP()<<endl;
}
int main(){
int t;
cin>>t;
while(t--){
solve();
}
return 0;
}