KMP算法详解

问题描述

  KMP是解决子串的定位操作的一种算法,即在一个字符串中找到另一个字符串出现的位置,如果找不到就返回-1.我们使用的例子如下:主串为ababcabcacbab, 子串为abcac。

符号标记

符号 | 描述 -- | -- S | 主串 T | 子串 i | 主串的下标 j | 子串的下标

传统做法

  传统做法同时遍历子串T和主串S,记i,j分被为主串和子串的下标,如果S[i]==S[j],则同时执行i++和j++;如果S[i]!=S[j]则将原先主串的开始遍历处的下标+1,重新开始比较。如下图:

当S[2]!=T[2]时,我们将T向右移动一个单位并重复上述操作。

这样不断重复直到子串全部匹配完毕返回子串位置,或者遍历超过了主串的长度,返回-1。

int indexOf_1(string S,string T){
int i=0;
int j=0;
while (i<S.size()&&j<T.size()){
if(S[i]==T[j]) {i++;j++;}
else {i=i-j+1;j=0;}
}
if(j>=T.size()) return i-j;
else -1;
}

int main(){
string S = "ababcabcacbab";
string T = "abcac";
cout<<indexOf_1(S,T)<<endl;
system("pause");
}

传统方法的弊端

  拿我们的例子来说,假如传统方法遍历时,出现了以下一幕:

我们比较了子串的a b c a 前五个字符但是但比较第5个字符时出现了不相等的情况,于是我们将子串向右移动一位再进行比较。这样不断重复直到:

现在我们可以思考一个问题,当我们遇到第一幅图的情况时,其实我们是已经知道移动子串两次的开始字符分别为b和c,这个信息就存储子串当中,因为我们经过第一幅图的比较就已经确定了子串的bc与主串的bc相匹配。那么我们怎么样利用上这个信息,使得子串的移动不是1个单位1个单位的移,而是根据上一次匹配得到信息,直接跳过某些字符的匹配。下面我们就来探讨下两个问题。

问题一:子串滑动多大距离

  当我们比较主串和子串的某个字符时,如果两个字符不相等我们应该将子串向右滑动几个单位。即若S[i]!=T[j],我们滑动子串使得S[0:i]=T[0:k],我们现在来确定k的大小。通过上一次不成功的比较,我们得到了如下结论S[i]!=T[j],因为子串已经比较到j了所以j前面的已经全都匹配上了。即:`S[i-j:i-1]==T[1:j-1]`。但我们滑动子串使得i与k相比较时,我们必须保证`S[i-k:i-1]==T[1:k-1]`.我们用下图来表示这三者之间的关系。

在j和k左边上下都是相互匹配的,通过图我们可以很清楚的看到,移动后的子串与原先的子串在绿色部分也是相互匹配的。即:T[1:k-1]=T[j-k+1:j-1]
  这样我们就可以通过一个next数组保存子串的匹配信息。next[j]=k表示当子串的T[j]与主串S[i]不匹配时,向右移动多个单位使T[k]与S[i]进行匹配。如果T[k]!=S[i],则继续使得T[next[k]]与S[i]相比较。
next的计算公式如下:

注:这里公式中的-1指的是当子串与主串在第一个字符处就不相等时,子串向右移动一个单位,即相当于原先子串-1处的字符与主串i处字符比较。实际上-1处是无效的。这里只是做一个标记,当遇到next[j]=-1时,子串滑动一个单位。

问题二:如何求子串的next值

  通过问题一中的分析我们可以知道子串中的next的值只与子串本身有关即`T[1:k-1]=T[j-k+1:j-1]`中只包含T的字符。现在我们可以确定的是next(0)=-1. 我们可以据此来完成递推。即已知next[j]=k,求next[j+1]。 分两种情况讨论。 * 当T[k]=T[j],这时next[j+1]=k+1=next[j]+1,画图表示如下。

其中蓝色部分表示T[1:k-1]=T[j-k+1:j-1]这是通过next[j]=k得出的。(这里需要注意的是需要理解next[j]=k只表示j(或者k)之前的一段字符相等,并不能得出k和j两处的关系,具体的公式得出可以看看问题一中的推导)。
这时若T[k]=T[j],图中的蓝色部分向右扩展一个单位得出T[1:k]=T[j-k+1:j]:

于是有next[j+1]=next[j]+1

  • 当T[k]!=T[j],next[j+1]=next[k]+1,k=next[k](重复使得T[next[k]]T[j])。这里也很好理解既然T[k]!=T[j],那我们就可以再从前面找到一个值k',使得T[k']T[j],k'的前面某一段必须与j前面相同的一段相同,同样也与k前面某一段相同,根据k,k'前拥有相同的一段就可以根据next[k]求出k'。

上面的两种情况我们也可以将其看作是子串和主串的匹配问题。只不过子串和主串都是T的某一段。

代码部分

//求next数组
void getNext(string T,int next[],int len){
next[0]=-1;
int k=-1;
int j=0;
while(j<len){
if(k==-1||T[j]==T[k]){ j++;k++;next[j]=k; }
else{
  k = next[k];
}
}
}
//匹配子串
int kmp(string S,string T){
int len = T.size();
int* next = (int*)malloc(sizeof(int)*len);
getNext(T,next,len);
int i=0;
int j=0;
while (i<S.size()&&j<T.size()){
if(j==-1||S[i]==T[j]) {i++;j++;}
else {j=next[j];}
}
int size = T.size();
if(j>=size) return i-j;
else return -1;
}

int main(){
string S = "ababaababcacbcacbab";
string T = "ababcac";
int index2 = kmp(S,T);
cout<<index2<<endl;
system("pause");
}

小结

  传统的算法的最坏情况下时间复杂度是O(m*n),当一般这种情况不多见。KMP算法的时间复杂度是O(m+n)。
posted @ 2019-04-29 22:10  Mrfanl  阅读(525)  评论(0编辑  收藏  举报