KMP算法详细解读

来源:互联网 发布:怎么开通淘宝直播间 编辑:程序博客网 时间:2024/06/05 02:51

备注:2016.11.18,写KMP算法千万不要忘了初始化next[0]=-1。


PS:自从学会了写伪代码,现在算法水平大大的提升了。KMP看了别人的博客看懂了,然后自己写一次性就AC了。

本文地址:http://blog.csdn.net/freeelinux/article/details/53154123


KMP入门请参考这几位大神:http://www.cnblogs.com/yjiyjige/p/3263858.html

http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

http://www.cnblogs.com/tangzhengyue/p/4315393.html

先总结思路:

求NEXT数组的伪代码是:
GETNEXT()
    j <- 0
    k <- -1
    next[0] <- -1
    while j < length -1 do
if k == -1 or p[j] == p[k]
   next[++j] <- ++k
else
   k <- next[k]
return next


例如: 
设主串下标为i,模式串下标为j,k是next数组对应的下标。符合条件k==-1 or p[j] == p[k]所走的分支下文称1分支,k<-next[k]所在分支称为2分支。


模式串: A B A B
0 1 2 3
首先该算法对next[0]初始化为-1,因为如果第一个字符不匹配,我们不可能向后回溯了。这个时候必须i <- i+1,j <- j+1,主串和模式串同时前进。
我们分循环走:
(1)第一次循环,j=0, k=-1,符合k==-1,所以走1分支,j和k分别加1,next[1]=0。在这里我们要理解一下,j和k的含义。我们KMP匹配凭借什么,就是凭借相同的前后缀,
    所以k初始为-1,j初始为0,一个在后面走,一个在前面走,可以凭借判断p[j] == p[k]来判断字符是否相等,计算前后缀相同的长度,进而得出我们的next匹配值。
    显然,第一次检测字符A,A前面没有任何前后缀相同,所以A的next值就是0。意味着A如果匹配失败,要从下标0重新开始匹配。我们想一下,A本来就是字符串第一个元素,
    我们在暴力匹配时,第一个字符匹配失败,不也正是再次用第一个字符和主串中下一个字符进行匹配吗?
(2)第二次循环,j=1, k=0,不满足k==-1,同时字符串下标为1的元素是B,不满足p[j]==p[k],所以进入2分支。因为k目前等于0,所以k=next[k]=next[0]=-1。然后再次进入
    while循环。此时我们惊奇的发现再次k==-1,所以next[++j]即next[2]=++k=0。我们来阐述一下它的道理,当刚进本次循环时,j=1,k=0比较字符A和B不等,这说明什么问
    题呢,说明B的下一个元素的前面元素前后缀相同的个数为0。是的,A,B没有一个相同的,下一个元素如果匹配失败,我们不能利用前后缀的特性少回溯一部分,必须从头
    重新开始比较,所以k=0。通过(1)(2)两步我想已经很明白了,只要一个元素前面的部分前后缀相同个数为0,那该元素的next值就为0。一旦该元素匹配失败,我们就相当
    于用暴力匹配的方法,再次比较。
(3)第三次循环,j=2, k=0, 这次p[j]==p[k]==A,所以走1分支,next[++j]即next[4]=++k=1,因为第三个元素A和第一个元素A相同,那么第四个元素前面元素前后缀相同的长
   度就为1。当第四个元素匹配失败时,由于我们知道它前面部分前后缀相同长度为1,在程序中表现为next值next[4]=1,所以我们只需从下标1的位置和当前匹配失败位置进行
   比较即可。
如: A B A C
    A B A D
这个匹配失败,那我们下一次直接这样:
    A B A C
        A B A D
        这样不是很好吗,下面的第一个元素A和上面的三个元素A既然相等,我们干嘛还要比较。通过next值,我们就可以知道不用比较哪些元素了,效率相比暴力解法有了很
   大提升,暴力解法时间复杂度是O(mn),而KMP算法的时间复杂度则是O(m+n)。


在上面中,我们给出了GETNEXT()函数,但是它还是有优化的余地的。
比如: A B A C
       A B A B
这样匹配,第二个B匹配失败,由我们之前的GETNEXT()方法求得的next数组为 {-1,0,0,1},所以B对应的next值为1,我们将从下标1重新开始匹配,但是
       A B A C
           A B A B
这样匹配有意义吗?明知道B已经在该位置匹配失败了。所以我们尽可能要略过此次比较,与其此次比较完,从next[1]=0重新开始比较,不如直接略过他,一步到位。
当出现p[++j]==p[++k]的时候,我们不再让next[++j]的值为为k,直接让它等于next[k]即可,就可达到略过的目的。
伪代码如下:
GETNEXT()
    j <- 0
    k <- -1
    next[0] <- -1       //不要忘了初始化这一步
    while j < length -1 do    //千万注意是小于length-1,而不是length,是从前一个推下一个,到不了下标为length-1的元素
if k == -1 or p[j] == p[k]
   if p[++j] == p[++k]
next[j] <- next[k]
            else
next[j] <- k
else
   k = next[k]
    return next


下面给出KMP算法伪代码
KMP(s1,s2)
    i <- 0
    j <- 0
    next[] <- GETNEXT()
    while i < s1.length and j < s2.length do
if j == -1 or s1[i] == s2[j]     //如果j==-1,j也要归为0,重新匹配
   i <- i + 1
   j <- j + 1
else
   j <- next[j]      //j的next值可能为-1
   if j == s2.length      //千万注意:当j==s2.length时才说明匹配完毕,而不是用s1比较

return i - j

   else return -1


代码如下:

#include <iostream>#include <string>#include <assert.h>int* get_next(const std::string& s, const int len){int *next = new(std::nothrow) int[len];assert(next != NULL);next[0] = -1;int j = 0, k = -1;while(j < len - 1){   //notice - 1, 不到达length-1元素if(k == -1 || s[j] == s[k]){if(s[++j] == s[++k])next[j] = next[k];elsenext[j] = k;}else{k = next[k];}}return next;}int KMP(const std::string& s1, const std::string& s2){const int len_s1 = (int)s1.length();const int len_s2 = (int)s2.length();if(len_s1 < len_s2)return -1;if(len_s1 == 0 || len_s1 == 0)return -1;int *next = get_next(s2, len_s2);int i = 0, j = 0;while(i < len_s1 && j < len_s2){if(j == -1 || s1[i] == s2[j]){++i;++j;}elsej = next[j];}delete []next;return j == len_s2 ? (i - j) : -1;    //记住判断j是否匹配完毕}int main(){//std::string s1("ABCABCDHABEJK");std::string s1("ABCABCEBD");std::string s2("BD");int res = KMP(s1, s2);std::cout<<res<<std::endl;return 0;}
0 0