kmp算法(上)

原创文章,转载请注明出处:http://blog.csdn.net/fastsort/article/details/9903153 

1、字符串匹配

所谓字符串匹配,就是在主串S中寻找模式串P,如果存在,则返回P在S中的起始下标,否则返回不存在信息(这里用-1表示)。例如:

S=abcdabce,T=cda,

则返回2,如果T=cdd,则返回-1.

考虑C/C++中字符串下标都是从0开始,以下讨论都是在字符串下标从0开始的前提下进行【有特殊说明的除外】。

 

2、常规算法

       在确定找到高效算法前,“暴力”算法(brute force)虽然效率差强人意,但是总是值得信任的。

       字符串匹配算法也是这样。直接匹配也很简单,把主串S的第一个字符和模式串P的第一个字符对齐,然后两个串同时向后比较:

                如果一直到P的结尾都匹配,很好,匹配成功,返回0;

                如果中间存在不匹配的字符,那么把S的第二个字符和P的第一个字符对齐,然后两个串同时向后比较,直到继续S的第三个字符或者是匹配成功返回1……

                重复上述过程,直到S的结尾,这时返回-1,说明匹配失败。

       代码如下:

int match_1(const char * s, const char *p)
{
    int slen=strlen(s),plen=strlen(p);
    int i=0,j=0;
    while(i<slen)
    {
        while(j<plen &&(i+j)<slen && s[i+j]==p[j]) j++;
        if(j==plen) return i;
        i++;
        j=0;
    }
    return -1;
}

这段代码运行良好,但是为了对比,将其改为如下形式:

int    match_2(const char * s, const char *p)
{
    int slen=strlen(s),plen=strlen(p);
    int i=0,j=0;
    while(i<slen && j<plen)
    {
        if(s[i]==p[j])///匹配
        {
            i++;
            j++;
        }
        else///不匹配了
        {
            i = i-j+1;///回退S的指针
            j = 0;///P从头开始
        }
    }
    if(j==plen)   return i-j;///匹配成功
    else       return -1;
}


简单的说,就是i,j初始时分别指向s和p的起始位置,匹配时i,j同时向后移动,失配时i指针回退,j指针置零

对于以上代码,你应当完全理解,给你笔和纸你就能准确无误的立刻写出来。这样你就对其工作过程了如指掌,否则后面的内容将难于理解。

 

复杂度分析:在最坏情况下,p每次都匹配到倒数第二个字符,然后最后一个字符不匹配,这时从头再来,显然每次p匹配都需要O(m)(其中m=strlen(p)),对s中的每个字符都匹配一遍,也需要O(n)(其中n=strlen(s)),总的复杂度就是O(mn)。

例如s=0000000000001,p=00001时,m=strlen(p)=5,n=strlen(s)=13.对于i∈[0-12],j∈[0-4],每次匹配到p[4]时,发现s[i]!=p[j]不匹配,都需要从头开始(j=0,i=i-j+1)下次匹配,即所谓的“回溯”。

我们发现,在这个过程中其实有很多回溯过程是没有必要的。比如:

i      0123456

s     0000000000001

p     00001

j      01234

在发现s[4]!=p[4]时,下次比较将从s[1]和p[0]比较开始:

i      0123456

s     0000000000001

p       00001

j        01234

一直比较到s[5]!=p[4]。其实如果注意到,在上一轮比较中,s[4]!=p[4]时(i=4,j=4),已经有

s[0...3]=p[0...3]       ①

所以有

s[1...3]=p[1...3]      ②

p[1...3]=p[0...2              ③

③中,p[0...2]为p的前缀,p[1...3]为后缀,这是由p本身的性质决定的,由②和③可以得到

s[1...3]=p[0...2]                               ④

看到这里你有什么感觉?既然s[1...3]=p[0...2]了,那为什么下一轮匹配过程还要从s[1]和p[0]开始呢?!直接比较s[4]和p[3](即s[i]和p[k],k=3)就可以了。kmp算法就是基于这个原理的,即发生失配后不需要每次都从p串的起始位置开始新的一轮比较(即i指针回退、j指针置零)

       上面这段分析,尤其是这三个等式,最好理解透彻。这里没有用各种ijk,而是用实际的字符串和数字表示,就是为了便于理解。理解透彻之后,就开始正式讲解kmp算法了。

 

3、kmp算法

       kmp算法对普通字符串匹配算法(即match_2)的改进之处在于:发生失配之后,i指针不动,j指针指向合适的位置,开始下一轮的匹配。

       假设合适的位置已经保存在一个数组next[n]中,n=strlen(p)。那么kmp的代码就是:

int match_3(const char * s, const char *p)

{

    int slen=strlen(s),plen=strlen(p);

    int i=0,j=0;

    while(i<slen && j<plen)

    {

        if(s[i]==p[j])///匹配

        {

            i++;

            j++;

        }

        else///不匹配了

        {

            //i =i-j+1; i指针不变,不再回退

            j = next[j];///j指针从某个位置开始

        }

    }

    if(j==plen)   return i-j;///匹配成功

    else       return -1;

}

发生改变的地方就在else里:i指针不再回退,j指针不是归零而是某个特定的值。看上去是不是比朴素匹配算法更简单?貌似是的,但是问题是这个特定值是什么?也就是说这个next数组怎么求呢?好,现在来说这个next数组。

 

       从第二部分的最后面的分析(就是①②③那三个式子那)我们知道,当s[i]!=p[j]时(失配了),已经匹配的部分为

s[i-j…i-1]=p[0…j-1]                                               ④

即s[i]和p[j]的前j-1个字符。

如果想从p的第k个字符开始下一轮的匹配而不是回溯,那么必须要满足:p的前k-1个字符和s[i]前面的k-1个字符匹配(即k=next[j])。

p的前k-1个字符为: p[0…k-1]

s[i]的前k-1个字符为:s[i-k…i-1]

也就是

p[0…k-1]=s[i-k…i-1]                                              ⑤

由④可得:

s[i-k…i-1]=p[j-k…j-1]                                             ⑥

即s[i-j…i-1]和后k-1个字符和p的后k-1个字符相等。

由⑤⑥得

p[0…k-1]=p[j-k…j-1]                                            ⑦

如果第一次看到这里,可能就有点晕了。我们再回顾下我们的问题。在p[j]处发生失配(s[i]!=p[j])后,i不动,j不需要置零,而是从k=next[j]处取得,那么这个k要满足⑦。

⑦是什么意思呢?左面是p的前k个字符(前缀),右面是p[0…j-1]的后k个字符(后缀)。这个k越大越好,为什么?因为越大j向后滑动的越多,效率越高。但是必须有k<=j-1 => k<j.

为便于理解,举个例子,对于p=”000011”:

j=0  ?

j=1  p[1]前有p[0]=p[0],                    next[1]=0

j=2  p[0-1]=p[0-1],                           next[2]=1

j=3  p[0-2]=p[0-2],                           next[3]=2

j=4  p[0-3]=p[0-3],                           next[4]=3

j=5  p[0…4]没有前缀=后缀,            next[5]=0


j=0时怎么办呢?考虑最原始的情况,s[i]!=p[0]了,第一个就不匹配,当然没法跳了,开始下一轮即可。所以这里可以设定一个特殊值,表示第一个(j=0)就不匹配,一般设置为-1,可以简化程序。在j=-1时,需要进行下一轮匹配(i++;j++)j++后刚好是0。如果你一定要设置为-9,那么你要判断,当j==-9时,下一轮要:(i++; j=0;)。你还会注意到,对任何长度大于2的p串,next[1]==0是恒成立的。

 

再来一个例子:

p[]=”abababacd”的next数组next[9]:

j=0,k=next[0]=-1;

j=1,k=next[1]=0

j=2,p[2]之前有”ab”,其前缀只有’a’!=后缀’b’,所以k=next[2]=0

j=3,p[3]前为”aba”,p[0]=p[2],                     k=next[3]=1

j=4,p[4]前为”abab”,p[0…1]=p[2…3], k=next[4]=2

j=5,p[5]前为ababa,p[0…2]=p[2…4],  k=next[5]=3

j=6,p[6]前为ababab,p[0…3]=p[2…5], k=next[6]=4

j=7,p[7]前为abababa,p[0…4]=p[2…6],k=next[7]=5

j=8,p[8]前为abababac,由于最后一个字母是c,任何前缀都没有和其相等的后缀,所以k=next[8]=0


相信通过这2个例子,你应该理解了next数组了吧。


由于next数组中存在j=-1的情况,所以我们的函数应该做少许修改:

int	kmp(const char * s, const char *p)
{
    int slen=strlen(s),plen=strlen(p);
    int *next = (int*)malloc(sizeof(int)*plen);
    getNext(p,next);
    int i=0,j=0;
    while(i<slen && j<plen)
    {
        if(j==-1|| s[i]==p[j])///j=-1时,也是进行下一轮匹配
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];///i,j不再回溯
        }
    }
    free(next);
    if(j==plen)  return i-j;
    else       return -1;
}


 

       我把函数名也修改为kmp,这也是kmp算法的最终形式(未优化的,后面再说优化的情况)。总共不到10行。

      其中的getNext()函数就是计算next数组的,如果你理解了next数组的计算方法,你可以动手写一下试试,其代码也不过十行,而且和上面的kmp函数惊人的相似。至于getNext()的思路和具体代码,还在留给下一篇blog吧。



原创文章,转载请注明出处:http://blog.csdn.net/fastsort/article/details/9903153 



  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值