kmp算法

来源:互联网 发布:图文编辑器软件 编辑:程序博客网 时间:2024/06/04 18:43

0、废话

 一直ym传说中的kmp算法能以最坏线性的时间复杂度搞定字符串匹配,

开始动手看才知道kmp中的K居然是Donald.E.Knuth,《计算机程序设计艺术》的作者。

好吧,继续ym……

1、传统的字符串匹配算法

复制代码
/*  * 从s中第sIndex位置开始匹配p * 若匹配成功,返回s中模式串p的起始index * 若匹配失败,返回-1 */int index(const std::string &s, const std::string &p, const int sIndex = 0){    int i = sIndex, j = 0;    if (s.length() < 1 || p.length() < 1 || sIndex < 0)    {        return -1;    }    while (i != s.length() && j != p.length())    {        if (s[i] == p[j])        {            ++i;            ++j;        }        else        {            i = i - j + 1;            j = 0;        }    }    return j == p.length() ? i - j: -1;}
复制代码

2、传统字符串匹配算法的性能问题

用模式串P去匹配字符串S,在i=6,j=4时发生失配:

                              i=6

S: a   b   a   b   c   a  d   c   a   c   b   a   b

P:           a   b   c   a  c

                              j=4

此时,按照传统算法,应当将P的第 1 个字符 a(j=0) 滑动到与S中第4个字符 b(i=3) 对齐再进行匹配: 

                 i=3 

S: a   b   a   b   c   a   a   d   a   c   b   a   b 

P:               a   b   c   a   c 

                 j=0 

这个过程中,对字符串S的访问发生了“回朔”(从 i=6 移回到 i=3)。

我们不希望发生这样的回朔,而是试图通过尽可能的“向右滑动”模式串P,让P中index为 j 的字符对齐到S中 i=5 的字符,然后试图匹配S中 i=6 的字符与P中index为 j+1 的字符。

在这个测试用例中,我们直接将P向右滑动3个字符,使S中 i=5 的字符与P中 j=0 的字符对齐,再匹配S中 i=6 的字符与P中 j=1 的字符。

                               i=6

S: a   b   a   b   c   a   d   c   a   c   b   a   b

P:                        a   b   c   a   c

                          j=0

3、kmp算法的一般性讨论

下面讨论在一般性的情况下,如何实现在“不回朔”访问S、仅依靠“滑动”P的前提下实现字符串匹配,即“kmp算法”

                               i=6

S: a   b   a   b   c   a   d   c   a   c   b   a   b

P:                        a   b   c   a   c

                              k=1

                                i=6

S: a   b   a   b   c   a   d   c   a   c   b   a   b

P:           a   b   c   a   c

                               j=4

对于任意的S和P,当S中index为 i 的字符和P中index为 j 的字符失配时,我们假定应当滑动P使其index为 k 的字符与S中index为 i 的字符“对齐”并继续比较。

那么,这个 k 是多少?

我们知道,所谓的对齐,就是要让S和P满足以下条件(上图中的蓝色字符):

……(1)

另一方面,在失配时我们已经有了一些部分匹配结果(上图中的绿色字符):

……(2)

由(1)、(2)可以得到:

……(3)

即如下图所示效果:

定义next[j]=k,k表示当模式串P中index为 j 的字符与主串S中index为 i 的字符发生失配时,应将P中index为 k 的字符继续与主串S中index为 i 的字符比较。

……(4)

按上述定义给出next数组的一个例子:

   j         0  1  2  3  4  5  6  7

   P        a   b  a  a  b  c  a   c

next[j]  -1  0  0  1  1  2  0  1

在已知next数组的前提下,字符串匹配的步骤如下:

i 和 j 分别表示在主串S和模式串P中当前正待比较的字符的index,i 的初始值为sIndex,j 的初始值为0。

在匹配过程中的每一次循环,若,i 和 j 分别增 1,

else,j 退回到 next[j]的位置,此时下一次循环是相比较。

4、kmp算法的实现

 在已知next函数的前提下,根据上面的步骤,kmp算法的实现如下: 

复制代码
int kmp(const std::string& s, const std::string& p, const int sIndex = 0){    std::vector<int>next(p.size());    getNext(p, next);//获取next数组,保存到vector中    int i = sIndex, j = 0;    while(i != s.length() && j != p.length())    {        if (j == -1 || s[i] == p[j])        {            ++i;            ++j;        }        else        {            j = next[j];        }    }    return j == p.length() ? i - j: -1;}
复制代码

ok,下面的问题是怎么求模式串 P 的next数组。

next数组的初始条件是next[0] = -1,设next[j] = k,则有:

那么,next[j+1]有两种情况:

,则有:

   此时next[j+1] = next[j] + 1 = k + 1

, 如图所示:

此时需要将P向右滑动之后继续比较P中index为 j 的字符与index为 next[k] 的字符:

 

值得注意的是,上面的“向右滑动”本身就是一个kmp在失配情况下的滑动过程,将这个过程看 P 的自我匹配,则有:

如果,则next[j+1] = next[k] + 1;

否则,继续将 P 向右滑动,直至匹配成功,或者不存在这样的匹配,此时next[j+1] = 0。

 getNext函数的实现如下:

复制代码
void getNext(const std::string &p, std::vector<int> &next){    next.resize(p.size());    next[0] = -1;    int i = 0, j = -1;        while (i != p.size() - 1)    {        //这里注意,i==0的时候实际上求的是next[1]的值,以此类推        if (j == -1 || p[i] == p[j])        {            ++i;            ++j;            next[i] = j;        }        else        {            j = next[j];        }    }}
复制代码

 至此,一个完整的kmp已经实现。

5、getNext函数的进一步优化

注意到,上面的getNext函数还存在可以优化的地方,比如:

                 i=3

S: a   a   a   b   a   a   a   a   b

P: a   a   a   a   b

                 j=3

此时,i=3、j=3时发生失配,next[3]=2,此时还需要进行 3 次比较:

i=3, j=2;  

i=3, j=1;  

i=3, j=0。

而实际上,因为i=3, j=3时就已经知道a!=b,而之后的三次依旧是拿 a 和 b 比较,因此这三次比较都是多余的。

此时应当直接将P向右滑动4个字符,进行 i=4, j=0的比较。

一般而言,在getNext函数中,next[i]=j,也就是说当p[i]与S中某个字符匹配失败的时候,用p[j]继续与S中的这个字符比较。

如果p[i]==p[j],那么这次比较是多余的(如同上面的例子),此时应该直接使next[i]=next[j]。

完整的实现代码如下:

复制代码
void getNextUpdate(const std::string& p, std::vector<int>& next){    next.resize(p.size());    next[0] = -1;    int i = 0, j = -1;    while (i != p.size() - 1)    {        //这里注意,i==0的时候实际上求的是nextVector[1]的值,以此类推        if (j == -1 || p[i] == p[j])        {            ++i;            ++j;            //update            //next[i] = j;            //注意这里是++i和++j之后的p[i]、p[j]            next[i] = p[i] != p[j] ? j : next[j];        }        else        {            j = next[j];        }    }}
复制代码

对应的,只需要在kmp算法中将 getNext(p, next); 替换成 getNextUpdate(p, next); 即可。

6、时间复杂度分析

下面以getNext函数为例,分析kmp算法的时间复杂度。

复制代码
 1 void getNext(const std::string& p, std::vector<int>& next) 2 { 3     next.resize(p.size()); 4     next[0] = -1; 5  6     int i = 0, j = -1; 7  8     while (i != p.size() - 1) 9     {10         if (j == -1 || p[i] == p[j])11         {12             ++i;13             ++j;14             next[i] = j;15         }16         else17         {18             j = next[j];19         }20     }21 }
复制代码

假定p.size()为m,分析其时间复杂度的困惑在于,在while里面不是每次循环都执行 ++i 操作,所以整个while的执行次数不一定为m。

换个角度,注意到在每次循环中,无论 if 还是 else 都会修改 j 的值且每次循环仅对 j 进行一次修改,所以在整个while中 j 被修改的次数即为getNext函数的时间复杂度。

每次成功匹配时,++i; ++j; , 由于 ++i 最多执行 m-1 次,故++j也最多执行 m-1 次,即 j 最多增加m-1次;

对应的,只有在 j=next[j]; 处 j 的值一定会变小,由于 j 最多增加m-1次,故 j 最多减小m-1次。

综上所述,getNext函数的时间复杂度为O(m),若带匹配串S的长度为n,则kmp函数的时间负责度为O(m+n)。

7、kmp的应用优势

①快,O(m+n)的线性最坏时间复杂度;

②无需回朔访问待匹配字符串S,所以对处理从外设输入的庞大文件很有效,可以边读入边匹配。


0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 小米2a开不了机怎么办 红米4x屏幕失灵怎么办 红米4x外屏坏了怎么办 honor手机开不了机怎么办 honor黑屏开不了机怎么办 红米2开机停在mi怎么办 红米手机无法开机怎么办 小米手机帐号密码忘了怎么办 小米手机忘记小米账号密码怎么办 小米4账号密码忘了怎么办 小米2a触屏失灵怎么办 红米note5拍照不清晰怎么办 红米手机太卡怎么办 红米3x忘记密码怎么办 红米手机太卡了怎么办 红米手机太卡怎么办? 红米3老是死机怎么办 红米3s经常死机怎么办 红米4a进水了怎么办 魅族耳机声音小怎么办 魅蓝note6声音小怎么办 魅蓝3s锁定怎么办 红米1密码忘了怎么办 小米手环升级失败怎么办 红米4忘记密码怎么办 红米1代忘记密码怎么办 红米3s忘记密码怎么办 小米3卡槽卡住了怎么办 红米4手机信号不好怎么办 红米note3信号差怎么办 红米note3没信号怎么办 红米3运行慢怎么办 红米手机运行慢怎么办 红米手机忘记解锁图案怎么办 小米手机解锁图案忘了怎么办 红米2内屏坏了怎么办 红米密码忘记了怎么办 红米pro玩王者卡怎么办 红米note4不支持计步怎么办 红米pro玩游戏卡怎么办 红米手机费电快怎么办