子串的匹配算法通常称为串的模式匹配或串匹配。通常使用的匹配算法有BF算法和KMP算法
主串S和模式串T,若匹配成功返回主串中首次出现的位置,否则返回-1;
BF算法:(暴力匹配算法)//只要失败就重新比较
时间复杂度:最优时间复杂度O(m+n);最坏时间复杂度O(mn);//n和m分别为主串和模式串的长度
算法步骤:(一般情况n>>m)
1)声明两个指针i与j分别指向主串S和模式串T当前待比较的字符位置。
2)如果i与j均未到达主串S和模式串T的尾部循环进行一下步骤:
如果S[i]==T[j]:则i++和j++; //两指针同时后移
反之S[i]!=T[j]:j=1,i=i-j+2;//j指针置1,i指针从上一次的下一个位置进行匹配
(推导:假设在j处失配,则在之前j-1必定匹配成功,那么在主串S中当前指针为i回退j-1位即到达上一次匹配位置进行+1操作便为下一次主串的匹配位置) 公式i=i-(j-1)+1 --> i=i-j+2;
循序结束时:如果j>T.length(模式串T的长度),说明匹配成功,返回i-T.length;
反之匹配失败,返回-1;
BF代码(c++):
int BF(string S,string T,int pos){//BF算法
int i=pos;//起始遍历位置
int j=1;//起始的模式串位置
while(i<=S.length()&&j<=T.length()){//均未到达两串的末尾
if(S.at(i-1)==T.at(j-1)){
i++;
j++;//相同比较下一个
}
else{
i=i-j+2;//回退到上一次的位置的下一个(i-j+2)
j=1;//重新匹配
}
}
if(j>T.length()){
return i-T.length();//返回的第一个字符的位置即为i的当前值减去模式串长度
}
return 0;//匹配失败返回0
}
注:虽然BF算法的时间复杂度为O(mn),但在一般情况下,执行时间近似于O(m+n)
KMP算法:KMP算法为BF算法的改进算法,是由Knuth、Morris、Pratt同时设计实现的,因此叫KMP算法
由于BF算法每次失配主串S都回退到上一次的下一个位置,模式串也回溯到起始位置,时间复杂度较高(举个例子)
1 2 3 4 5 6 7 8 9 10 11 14 15
S:a b a b c a b c a c b a b //主串S
T:a b c a c //模式串T
第一次匹配失败时:当匹配到第3位时(c!=a)BF算法中主串回溯到第二位,模式串回溯到第一位,但显然b!=a
第二次匹配失败时:当匹配到j=5,i=7时(b!=c)BF算法中主串回溯到第四位,但是显然i=4、5位都不与j=1匹配。
以上问题说明BF算法不停的回溯,其实之前有很多元素都不用进行二次比较,能不能i指针不产生回溯,只是进行j指针的
移动打到匹配效果。即模式串不停的由移动与主串达到匹配。利用已经得到的“部分匹配”的结果,使得在比较过程中
模式串尽量大的向右移动,由此产生了KMP算法;
特点:主串S的i指针不回溯,模式串的j指针改变;通过部分匹配的结果让模式串尽可能的向右滑动。
下标: 1 2 3 4 5 6 7 8 9 10 11 12 13
主串S:a b a b c a b c a c b a b
模式串T:a b c a c
主串与模式串的指针分别为i与j初始化为每个串的第一个元素下标 即i=j=1
主串中的i指针不回溯一直进行自加操作(++);模式串中的j指针回溯。
第一次匹配:i从1开始;当i=j=3时,模式串第三位匹配失败。
i i=3
a b a b c
a b c
j j=1..3
j指针回溯到1重新匹配,模式串向右滑动2位,j=1开始 //j=j-2
(j为已经匹配的字符数+1)
第二次匹配:i从3开始;当i=7,j=5时匹配失败,模式串第5位匹配失败。//j=j-3
(j为已经匹配的字符数+1)
i i=7
a b a b c a b c a c
a b c a c
j j j=1--5
经分析得到如果按照BF算法中i要回溯到4则i=4,i=5都与模式串的j=1不匹配,所以比较没有意义,浪费资源
j指针回溯到2重新匹配,模式串向右滑动3位,j=2开始
第三次匹配:i从7开始;模式匹配成功
i i=10
a b a b c a b c a c
a b c a c
j j j=1--5
一般情况:
假设主串为s1s2s3...sn
模式串为t1t2t3...tm (n>>m)主串一般远大于模式串
目前要解决的问题是:当匹配失败时j要回溯到何处与i值进行匹配(即模式串的移动,j中的那一个元素与i对应)
假设此时与模式串的第k个元素进行比较(k
(k-1为比较成功的字符串长度,比较第k位时主串中的指针一定在第i位,所以成功比较的元素在主串中
的位置范围为[i-k+1,i-1])
由部分匹配的结果:T[j-k+1...j-1]==S[i-k+1...i-1] //k为相同前缀与后缀子串的最大长度
(第j位置匹配失败,前k-1项匹配成功T串中范围[j-k+1,j-1]) //(关系式2)
得到:T[1...k-1]==T[j-k+1...j-1](关系式3) //k位之前的k-1个字符与j位之前的k-1个字符相同
(记住关系式3这是求解k的一个条件 存在前后缀最大相同子串)
结论:在匹配过程中主串中的第i个字符与模式中的第j个字符比较不等时(S[i]!=T[j]),仅需要将模式(模式串向右移动的位数为:已匹配的字符数-失配字符上一位字符所对应的前后缀公共元素的最大长度)
定义next函数
0 j=1(t1与si比较不同时,下一步比较t1和si+1)
next[j]=Max{k|1
1 k=1(不存在相同的子串,下一步比较t1和si)
当为0时为第一个元素的初始化;k=1表明为第一个元素无相同子串next[j]置1;其他说明存在前后缀最大子串,为最大的长度
总之next计算方式:(next数组的值只与模式串有关)
初始化--> next[1]=0;
如果T[i]==T[j] 则i++,j++ next[j+1]=next[j]+1
如果T[i]!=T[j] 则j=next[j]; //一直回溯到0之后置1 否则为next[j+1]=next[k]+1; k为中间回溯位置
KMP算法代码:
int KMP(char s1[],char s2[],int pos){//pos为匹配的起始位置
int i=pos; //s1为主串,s2为模式串
int j=1;
while(i<=strlen(s1)&&j<=strlen(s2)){
if(j==0||s1[i-1]==s2[j-1]){//或者j回退到0
i++;
j++;//如果匹配相同,同时后移
}
else{
j=next[j];//如果不同j回退到next[j]处于i位置元素重新比较
}
}
if(j>strlen(s2)){//匹配成功
return i-strlen(s2);//i当前位置减去串长即为初始位置
}
return 0;
}
next数组的计算:
void getnext(char s[]){
//由模式确定next
int i=1;
next[1]=0;//初始化为0
int j=0;
while(i
next数组的优化:
void getnextal(char s[]){
//由模式确定next
int i=1;
next[1]=0;//初始化为0
int j=0;
while(i
abc a 123456 45 abc ddd
1 4 -1
#include
#include
#define N 1000005
char s2[N];//模式
char s1[N];//主
int next[N];
void getnext(char s[]){
//由模式确定next
int i=1;
next[1]=0;//初始化为0
int j=0;
while(istrlen(s2)){//匹配成功
return i-strlen(s2);//i当前位置减去串长即为初始位置
}
return -1;
}
int main(){
while(scanf("%s",s1)!=EOF){//主
scanf("%s",s2);//模式串
getnextal(s2);
printf("%d\n",KMP(s1,s2,1));
}
return 0;
}