超清晰KMP详解

KMP算法详解

 

记得之前本科的时候看KMP每次都看的云里雾里,似懂非懂。今天再拿起来看一下,发现有了清晰的理解。

 

NEXT数组

想掌握KMP算法我们需要要非常了解这样一个数组。假设有一个字符串s,我们称s[0~i](i<s.size())为s的前缀,同样的后缀定义为s[j~s.size()-1]。其中i不能取到s.size()-1,j不能取1。举个例子:

abcde:

前缀可以是:a,ab,abc,abcd。

后缀可以是:e,de,cde,bcde。

而我们的next数组的第i个数为:字符串s[0~i]的前缀s[0~j]等于后缀s[(i-j)~i]的最大j值。如果没有这样一个值则next[i]=-1。正好next[i]为所求前后缀相等的前缀的最后一位下标。

以字符串s="abaabaabab"为例:

我们的next数组由上图组成,next[i]的值为s[0~i]前后缀相等的最长前缀下标(最后一位下标)。那我们应该这么求next数组呢?我们可以用递推的方式,通过已知的next[0]~next[i-1]来求next[i]。这也是用到了动态规划的思想。

我们假设已经有了next[0]=-1,next[1]=-1,next[2]=0,next[3]=0。来求解next[4]。如下图:

当已经得到next[3]=0时,表示最长相等前后缀为"a",在之后计算next[4]时,判断得s[4]==s[next[3]+1]。因此可以吧最长前后缀延伸至"ab"。并使next[4]=next[3]+1,且令j指向next[4]。并且按照此方法可以一直求解到j=next[8]=5。但是当求解next[9]时,发现s[9]!=s[j+1],即s[9]!=s[6]。这时该如何处理呢?这里我们有一个好办法去缩短j的值,去重新满足s[9]==s[j+1]。如下图所示:

这里的处理方法用符号‘~’表示。处理过程是:我们首先发现在i=9时,s[9]!=s[next[8]+1]。我们想让前缀那一排(下排)右移一定位置,即获取一个新的j,使得s[9]==s[j+1]。获取新j的方式可以把s[0~next[8]]看作一个新字符串,然后去使其前后缀相等(下图绿框处)。而正好该新字符串的前后缀相等最大j已经保存于next[next[8]]=next[5]=2。该过程如下图:

然后我们仍然发现s[9]!=s[2+1],因此我们需要一直迭代,直到获得的j使s[9]==s[j+1]或者j=-1才停止循环。我们例子中再进行一次迭代正好满足s[9]==s[0+1],如下图:

我们现在已经清楚了已知next[0~(i-1)]可以求得next[i]了。我们的起始点next[0]=-1,这是一定的,因为单个字符没有前后缀。这样我们就可以获得整个next数组了。我们根据上面的理解,列出求解next数组的算法公式:

  1. 初始化next数组,令next[0]=-1。
  2. 使i在1~(s.size()-1)范围内遍历。利用步骤3.和4.计算next[i]值。
  3. 如果s[i]!=s[j+1]并且j!=-1,则循环执行j=next[j]。直到s[i]==s[j+1],或者j=-1。
  4. 如果s[i]==s[j+1],则next[i]=j++。否则next[i]=j。

我们写出获取next数组的代码:

const int MAXN=10000;
int nextT[MAXN];

void getnext(char* s,int len){
    nextT[0]=-1;
    int j=-1;
    for (int i=1; i<len; i++) {
        while (j!=-1&&s[i]!=s[j+1]) {
            j=nextT[j];
        }
        if (s[i]==s[j+1]) {
            j++;
        }
        nextT[i]=j;
    }
}

 

KMP算法

现在来看KMP算法,其实KMP就是刚才提的获取next数组算法的延伸。这里我们给出文本串"text"和匹配串"pattern",然后判断pattern是不是text的子串。我们设"text"="abaabaabc","pattern"="abaabc"。如下图所示:

其中i为指向"text"当前欲比较位,j为"pattern"中已完成匹配的最后位。只要满足text[i]==pattern[j+1],则表示当前位匹配完成,使i++,j++进行下一位比对。直到j=Plen-1(Plen是pattern的长度)。

但当我们继续比对i=5,j=4时,发现text[5]!=pattern[4+1]。我们如果采用暴力法,则只能从头开始匹配,但是KMP则避免了这种方法,它采用了刚才提到的next数组方法。我们可以回退尽量少的字符,然后重新去比对下一个。是不是很熟悉。观察下图:

我们回退的正好是pattern[0~4]前后缀相等的最大下标值(我们还是用j表示)。即j=next[j]。我们然后继续判断text[i]==pattern[j+1],在此例子中为text[5]==pattern[1+1]。结果正好匹配。如果不匹配,则继续执行回退,直到text[i]==pattern[j+1],或者j=-1。到这里,我们KMP的思路已经很清晰了:

  1. 初始化j=-1,表示pattern当前已经匹配到的最后位为-1。
  2. 让i遍历整个text,对每个i,执行3.和4.来匹配text[i]和pattern[j+1]。
  3. 如果j!=-1并且text[i]!=pattern[j+1],则循环执行j=next[j]。直到s[i]==s[j+1],或者j=-1。
  4. 如果text[i]==pattern[j+1],则j++,若j==Plen-1,则表示字符串匹配成功。
  5. 如果i的整个text遍历完了也没有j==Plen-1,则字符串匹配失败。

我们KMP算法代码如下:

const int MAXN=10000;
int nextT[MAXN];

void getnext(char* s,int len){
    nextT[0]=-1;
    int j=-1;
    for (int i=1; i<len; i++) {
        while (j!=-1&&s[i]!=s[j+1]) {
            j=nextT[j];
        }
        if (s[i]==s[j+1]) {
            j++;
        }
        nextT[i]=j;
    }
}

bool KMP(char*text,char*pattern,int Tlen,int Plen){
    int j=-1;
    getnext(pattern, Plen);
    for (int i=0; i<Tlen; i++) {
        while (j!=-1&&text[i]!=pattern[j+1]) {
            j=nextT[j];
        }
        if (text[i]==pattern[j+1]) {
            j++;
        }
        if (j==Plen-1) {
            return 1;
        }
    }
    return 0;
}

KMP算法的时间复杂度直接从暴力法的O(nm)降到O(n+m)。真的是非常的快速的算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值