KMP 算法

(KMP算法并不很好理解,请读者在阅读过程中集中精力并有自己的思考)

这是一种神奇的算法----

首先解释一下KMP算法是干什么的。

你有两个长度不同字符串S、T,它们的长度分别为len1/len2,你要判断T是否为S的子串(即S中是否包含T)。

以人类的思维是是这样进行判断的:一位一位比较。然而这样速度太慢了,因为在最坏的情况下你最多可能需要比较进行(len1-len2)*len2次比较(想一想,为什么)。

那你有没有思考,为什么会很慢呢?是因为在此过程中,你做了很多重复的工作,而KMP算法就是用来尽量缩减这些重复工作的。

先举个例子



若S=”aaaaaaaaaaab” T=”aaaaab” 现在逐位进行比较,到第五位时就会失配。
这里写图片描述
将T后移,若直接逐位比较,那么又要比较五次才会发现仍会在第五位失配
这里写图片描述
那么容易发现前四位的比较并不是必要的,为什么呢?我们会发现因为T[1..3]=T[2..4],即S[1..3]=S[2..4],又已验证S[1...3]=T[1...3],所以S[2..4]=T[1..3],再进行比较只会浪费时间。
再来一个更普遍的例子:

S=”abaaaababb”,T=”abaab”
这里写图片描述

逐位比较,在第五位发现失配,下面可直接作出如下操作:
这里写图片描述
使之后直接进行“有用”的第二位操作。

可能有人会产生疑问,为什么T串要移动到这个位置?我们观察T串会发现,在已进行匹配的五位中,T[1..2]=T[4..5](),我们已发现S[4]与T[4]匹配,而T[1]=T[4],故可将T[1]移到S[4]直接进行下一步比较。

下面介绍如何算出后移位数。

·NEXT数组的计算

我们会发现,要后移的位数只与T串有关,找出T串相同的前缀和后缀长度(且不能为自己,想一想,为什么),即可以算出最佳的后移位数。

我们用next[x]表示当T的前x位最长相同前缀和后缀的长度,即当T第x位失配时应该后移x-next[x]位。

能不能直接求呢?当然可以,但这样同样太慢了,会使得KMP算法的优越性不复存在。

那么如何效率算出f数组呢?

我们假设已经计算好前7位,且next[7]=3,现在计算第8位,如图所示

(灰色为已匹配位数)
这里写图片描述


若此时T[4]=T[8],可直接求出next[8]=next[7]+1=4

然而有时情况并非这样,若T[4]!=T[8]

那么我们查看next[next[7]],即next[3],我们假设next[3]=2,即T[1..2]=T[2..3],相应的,我们有T[5..6]=T[6..7],他们与T[1..2]也相等。
这里写图片描述


    若此时T[3]=T[8],即T[1..3]=T[6..8],可直接求出next[8]=next[3]+1=3

但若T[3]!=T[8],我们继续查看next[next[3]],即next[2]


…(以此类推,进行“前缀的配置”,直至配置成功)


至此,你有没有懂next数组的计算原理了呢?多模拟上述过程,加强自己的理解。(*形成自己的理解很重要)

下面给出代码:
void Make_Next(char T[],int next[]){
    int len=strlen(T);
    next[0]=0;                                      //边界
    for (int i=0,k=0;i<len;i++){
        while(k && T[i]!=T[k]) k=next[k-1]; //上面图示过程
                                            //注意k为非负数
        if(T[i]==T[k]) k++;                 //进行匹配
        next[i] = k;                        //保存
    }
}
根据以上思路,我们自然而然的就可以写出整个程序了,在操作中,我们将“串的移动”操作为k指针的移动。下面给出代码参考。然而在继续阅读前,建议读者仿照上述思路先自己手写一遍代码。
#include<cstring>
#include<iostream> using namespace std;

const int maxn=100;
int next[maxn];
char S[maxn],T[maxn];

void Make_Next(){
    int len=strlen(T);
    for(int i=1,k=0;i<len;i++){
while(k && T[i]!=T[k]) k=next[k-1];
        if(T[i]==T[k]) k++;     
        next[i]=k;
    }
}
bool Find(){
    int len1=strlen(S),len2=strlen(T);
    for(int i=0,k=0;i<len1;i++){
        while(k && S[i]!=T[k]) k=next[k];
                                        //若失配,进行“前缀的配置”
        if(S[i]==T[k]) k++;
        if(k==len2) return true;        //找到子串,返回真
    }
    return false;
}
int main(){
    cin>>S>>T;
    Make_Next();
    if(Find()) cout<<"Yes";
    else cout<<"No";
    return 0;
}


完成后,相信你已发现制作next数组和查找子串的代码非常相似(这也是KMP算法的神奇之处之一)。你也可以稍微改动一下程序,使得程序输出T在S串中第一次出现的位置或出现的次数(这并不困难)。

至此,我们可以分析一下KMP算法的复杂度。我们可以这样看,应变量i和k在运算中不断增加直至达到字符串长度,所以KMP算法的复杂度线性的,已经达到很优。

KMP算法的优点在于代码短且复杂度低,而缺点有难于理解和代码容易打错,希望鄙人此文能有助于大家对于这一算法的理解。

–Foggy
2015.4.17

此博客中的热门博文

图论专题总结