字符串比较,是实际应用中比较重要的一个内容,如果单纯的逐位比较,显然是太慢了(当然,朴素的字符串比较算法在平均情况下表现还是很好的)。下面要介绍的是KMP算法。
思想
KMP算法的思想很简单,它最核心的思想就是当失配时利用当前的信息而不是重新从头比较。考虑A串和B串,要求B在A第一次出现的位置(通常称B串为模式串)。令p[i]表示小于i的最大的x,使得[1,x]=[i-x+1,i](如何求p数组的问题等下再探讨)。那么,如果失配(A[i]≠B[j]),在这种情况下,朴素算法将令j=1,重新比较,但是,KMP算法利用了已经处理过的信息,即 B[1,j−1]=A[i−j+1,i−1] ,所以,不需要重新比较,我们就可以知 B[1,p[j−1]]=A[i−p[j−1],i−1] ,我们的目的,就是要找到B的一个最大的前缀[1,x],使得 B[1,x]=A[i−x+1,i] ,所以,只要不停地重复取p[],直到 B[p[j]+1]=A[i] 即可。代码如下:
int j=0;
for(int i=1;i<=n;i++){
while(j&&s1[i]!=s2[j+1])j=p[j];
if(cmp(s2[j+1],s1[i]))j++;
if(j==m){
printf("%d",i-m+1);
return 0;
}
}
如何求p数组
要求p[i]我们可以利用p[1..i-1],我们知道,p[i]表示的是使前缀等于后缀的最大长度,所以,最好的情况一定是 B[p[i−1]+1]=B[i] ,这样,最大长度就是 p[i−1]+1 ,但是, B[p[i−1]+1] 不一定等于 B[i] ,如果不等于,怎么办,显然,我们需要所小长度,但是,缩小到多少呢?答案是应该缩小到p[p[i-1]]。代码如下:
p[1]=0;
for(int i=2;i<=m;i++){
int j=p[i-1];
while(j&&!cmp(s2[j+1],s2[i]))j=p[j];
if(cmp(s2[j+1],s2[i]))p[i]=j+1;
}
有没有发现,这段求P数组的代码和上面的比较的代码惊人的相似。这是因为,求P数组实际上就是一个对模式串进行自我匹配的过程。
时间复杂度
KMP的时间复杂度是线性的,即是
O(N)
,N为主串长度。为什么?最难估算复杂度的地方就是循环中的while语句,我们看,每次while,j都会变小,并且,j变到0就停了,要变小,j首先得加上去,由于每次循环j最多加一,所以j最多累积加了N,同理,j最多累积减小了N次,所以KMP算法的构造P数组和求解的过程复杂度都为
O(N)
。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 100006
using namespace std;
inline char nc(){
static char buf[100000],*i=buf,*j=buf;
return i==j&&(j=(i=buf)+fread(buf,1,100000,stdin),i==j)?EOF:*i++;
}
inline int read_s(char *x){
char ch=nc();int len=0;
while(ch!='\n'&&ch!=EOF)*x++=ch,len++,ch=nc();
*x='\0';
return len;
}
int n,m,p[maxn];
char s1[maxn],s2[maxn];
int main(){
freopen("str.in","r",stdin);
freopen("str.out","w",stdout);
n=read_s(s1+1);m=read_s(s2+1);
p[1]=0;
for(int i=2;i<=m;i++){
int j=p[i-1];
while(j&&s2[j+1]!=s2[i])j=p[j];
if(s2[j+1]==s2[i])p[i]=j+1;
}
int j=0;
for(int i=1;i<=n;i++){
while(j&&s2[j+1]!=s1[i])j=p[j];
if(s2[j+1]==s1[i])j++;
if(j==m){
printf("%d",i-m+1);
return 0;
}
}
printf("-1");
return 0;
}