KMP算法的一些误区及其优化

来源:互联网 发布:星际航行概论 知乎 编辑:程序博客网 时间:2024/05/26 17:49

      16年12月26日再次编辑

昨晚在看清华严蔚敏的数据结构,重温了一遍KMP算法。严奶奶讲的特别详细特别透彻。发现自己还是有一些东西想岔了,这里重新修补下原来的博文。

问题主要集中在next[]数组的优化方面,在原来的地方用红色重写一下。



                 *********************************我是原来博文的分隔符********************************

这两天在看哈工大王宏志老师的算法设计入门课,讲到了KMP算法,这个之前也学习过,但一直没有搞的特别明白。于是花了两天的时间,看博客什么的,比如http://www.aboutyun.com/thread-9994-1-1.html就讲的很好。总之现在终于稍微清楚一点了,有一些相关的误区和优化想记录下来.。代码均在xcode(GCC)上可通过,都是很浅的东西,不知道有没有比我还小白的人看。

1. HOW TO GET next[]

   (1) 明确next数组是针对子串PATTERN的,与原字符串S无关!//有一个相互匹配的问题,两个字符串,一般长的是S。或者,不重复的是S。(为什么?因为复杂度与p的长度有关,也与重复性有关。)

  (2)next数组记录的是前缀-后缀的最大长度,而不是个数. 前缀中每一个都含有首字符,后缀中每一个都含有末字符。比较时是类似队列那种依次向后的比较方法。可以分为4种情况:

    (3) 最笨的方法,非递归,没有出现k=next[k] or k=next[k-1] 这种情况


void GetNext(char* p,int next[]){    next[0] = 0;    int k = 0;    int j = 1;    while (j < strlen(p) )    {        //k是前缀下标,j是后缀下标        if(k!=0 && p[k]!=p[j])  //分四种情况,根据k是否等于0(或>0,一样的,因为K不会<0)和P[K]是否等于p[j]。        {            k--;    //唯一一个需要回溯k的,如“aadaaa",最后一位a与d不匹配,则缩小前缀比较前一个是否与最后的a匹配。            if (p[j]==p[k])             {               if(k!=0)               {next[j]=k;  //记录此时的前缀,如"aadaaa",记录1,暂时是错误的,会在下一次循环中再加1.                                }               else if(k==0)               {next[j]=1;    //或=k+1,这里是可以合并简化的点               }                            }        }        else if(k==0)        {   if(p[j]==p[k])            {next[j]=1;     //或=k+1,这里是可以合并简化的点                k++;}            else next[j]=0;  //或=k,这里是可以合并简化的点            j++;        }        else {next[j]=k+1;   //即k!=0 && p[j]=p[k]时,或为next[j]=next[j-1]+1            j++;            k++;}         }    for (int j=0; j<strlen(p); j++)        printf("%d  ",next[j]);}
也可以不依赖k,所有next[j]都用next[j-1]或数字来表示(不用前缀,用后缀)

    while (j < strlen(p) )    {        //p[k]表示前缀,p[j]表示后缀        if(k!=0 && p[k]!=p[j])        {            k--;            if (p[j]==p[k])            {               if(k!=0)               {next[j]=next[j-1];                   j++;                }               else if(k==0)               {next[j]=1;               }                            }        }        else if(k==0)        {   if(p[j]==p[k])            {next[j]=1;                k++;}            else next[j]=0;            j++;        }        else {next[j]=next[j-1]+1;            j++;            k++;}         }
(4)
好了,非递归的方法很笨,接下来递归。

前后不变,中间改为:

 for (j=1;j < strlen(p);j++ )  //for循环更清晰    {        while (k>0 && p[k]!=p[j])  //也可以用if,其实是一样的,while会减少循环次数        {            k=next[k-1];        }        if (p[k]==p[j])        {            k++;        }        next[j]=k;    }


2.  优化kmp数组。

关于为什么要优化和另一种k=-1,next[0]=-1的写法,可以参考上文提到的http://www.aboutyun.com/thread-9994-1-1.html。

这里转载一下代码   (严蔚敏的数据结构课算法与之类似,求得的next数组首位是0,1位是1,此后为该位之前的最大相同前后缀长度+1)貌似用||的写法才能在next数组中加上优化。

void GetNextval(char* p, int next[])    {        int pLen = strlen(p);        next[0] = -1;        int k = -1;        int j = 0;        while (j < pLen - 1)        {            //p[k]表示前缀,p[j]表示后缀              if (k == -1 || p[j] == p[k])            {                ++j;                ++k;                //较之前next数组求法,改动在下面4行                if (p[j] != p[k])                    next[j] = k;   //之前只有这一行                else                    //因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]                    next[j] = next[k];            }            else            {                k = next[k];            }        }    }    

这里的优化是通过对next[]数组的改动完成的,在KMP中仍然是 j=next[j]。但这里的next数组相当于最大前后缀右移一位,左边补

上-1,如果想直接利用最大前后缀来计算,不妨在KMP函数中优化。代码为:

void KmpSearch(char* s, char* p){    int i = 0;    int j = 0;            while (i<strlen(s)&&j<strlen(p))    {        if (s[i] == p[j])        {        j++;        i++;        }        else if(j>0)            {                if(next[j]==0) j=next[j-1];                while(next[j]>0)                {                    j=next[next[j-1]-1]; //不断递归                }                        }        else if(j==0)        {            i++;        }    }    if (j == strlen(p))    {printf("%d\n" , (i - j));j=0;}    else        printf("-1");}

回过头来说说k=-1时next数组的优化,这里的-1其实对每个重复的字符头记录了“头”的位置,比如”ABCABCABC“,

对应”-1 0 0 -1 0 0 -1 0 0“,而不是 ”000123456“这点很妙,等于当末位(最坏时)失配时,直接回到头了,而不用再比一次,发现

ABC==ABC,还是不匹配,再从头开始比。

例子char p[]="abcabc";
    char s[]="abcababcabc"; 可以自己动手试试。

严蔚敏的数据结构课是这么解释的,对每一个字符(第i位),找它next[I]位的字符,如果二者重复则用next[next[i]]来替换next[i],相当于再次递归next[],我原来觉得是只往回找了一次,但其实回找的那个字符也对应的是它之前的 字符(如重复),所以效率提高是彻底的。

课上举的例子,“aaaab”,优化前对应01234(即-1 0 1 2 3,因为严奶奶的课字符串首位是字符长,第一位,即真正的第一个字符,对应第1位而不是第0位),优化后对应 00004(-1 -1 -1 -1 3)

而上述代码,则是在重复子串失配时,不断递归到上一次的位置,如”ABCABCABC“,假如在末位“C”不匹配时,用第一个C代替(肯定还是不匹配)。没有直接用k=-1时的代码效率高,但也只差1次。为什么呢?比如“ABABABAB",重复偶数次,当最后一个B不匹配时,j可以递归到0,而当”ABABAB",即,重复奇数次时,j只能到第一个“AB”的“B”,再与s比较。最多一次结束。可以考虑一下,这种最坏情况是什么样子?


3.什么是最坏,以及时间复杂度

明天再写

算了随便写写得了

(1)每次动j-next[j]位,j越大,next[j]越小,动的越少,比较的次数就越多,算法越慢。next数组体现了P的重复度,对没有优化的KMP,重复高,nexT值大,最坏的情况就是 一个子串 p=aaaaaaa,然后s=aaaaaaab.....这样子,next[j]==j-1,比到最后一位发现不对,只能挪

1位,那就退化到暴力搜索了。。。。时间复杂度仍不是线性的。不过这种情况相对罕见。

(2)对优化后的,有兴趣的自己算一算吧。


总之,优化还是很重要的! 不对的地方欢迎指正。


但愿这下没有不对的地方了。。。。

 



   

 


0 0