zoukankan      html  css  js  c++  java
  • KMP算法分析

    文中的公式图片在chrome浏览器下显示不正常,IE则显示正常。

    一、引言

    主串(被扫描的串):S=clip_image002[15],i 为主串下标指针,指示每回合匹配过程中主串的当前被比较字符;

    模式串(需要在主串中寻找的串):P=clip_image004[15],j 为模式串下标指针,指示每回合匹配过程中模式串的当前被比较字符。

    字符串匹配:在主串中扫描与模式串完全相同的部分,并返回其在主串中的位置,这里的起始扫描位置默认为主串的第一个字符,即默认pos=1,其他情况类似。

    朴素匹配算法:在模式串与主串的匹配过程中,一共要进行n=Length(S)回合的匹配,每一回合分别从主串的起始字符clip_image006[14]clip_image008[14]、...、clip_image010[14]开始进行。在具体某一回合的匹配过程中,每当模式串P中的某一字符与主串S中的被比较字符不相等,主串S的指针 i 都必须回溯到此回合起始字符的下一个位置,模式串P的指针 j 回到模式串串首,重新进行下一回合匹配。算法最坏情况下的时间复杂度为O(m*n)。这里不再详述。

    KMP匹配算法:KMP是一个高效的字符串匹配算法,它是由三位计算机学者 D.E.Knuth 与 V.R.Pratt 和 J.H.Morris 同时发现的,因此人们通常简称它为 KMP 算法。在KMP匹配过程中,每当模式串P中的某一字符与主串S中的被比较字符不相等,主串S的指针 i 不需要回溯,而只要将P串“向右滑动”到一定位置,继续进行下一回合的比较。KMP算法的时间复杂度为O(m+n)。

    下面主要理解KMP匹配算法。

    1)先由KMP算法的主要思想得到next函数的定义

    2)然后根据next函数定义求取next函数值

    3)最后根据next函数值进行主串、模式串匹配

    4)文章最后,根据next函数定义一眼看出来next函数值(有待考证)。

    二、定义next[j]

    我们要解决的关键问题是:当本回合匹配过程中出现失配时,下回合匹配时模式串“向右滑动”的可行距离有多远,也即:本回合匹配过程中,主串S的第 i 个字符与模式串P中的第j个字符失配时,下回合匹配时主串中的第i个字符应与模式串中的哪个字符进行比较(设为next[j],因为是“向右滑动”,故next[j]<j)。

    假设有以下主串和模式串:

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    P:

    a

    b

    a

    a

    b

    c

    a

    c

                 

    j:

    1

    2

    3

    4

    5

    6

    7

    8

                 

    a)。。。之前匹配步骤

    b)在经过若干回合匹配之后,两字符串状态如下:

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    P:

     

    a

    b

    a

    a

    b

    c

    a

    c

               

    j:

     

    1

    2

    3

    4

    5

    6

    7

    8

               

    c)此时主串的第i(i=2)个字符与模式串的第1个字符不等,主串的指针i不回溯,从而保持不变,由于next[j]<j,模式串指针j向右移动至位置next[1]= 0,情形如下。考虑定义:next[j] =next[1]= 0。(位置"j=0"是假想的,在后面我们将会发现这样处理的好处)

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    P:

       

    a

    b

    a

    a

    b

    c

    a

    c

             

    j:

     

    0

    1

    2

    3

    4

    5

    6

    7

    8

             

    d)令i和j均自增,继续进行比较,情形如下:

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    P:

       

    a

    b

    a

    a

    b

    c

    a

    c

             

    j:

       

    1

    2

    3

    4

    5

    6

    7

    8

             

    e)此时字符相等,主串的指针i和模式串的指针j均自增1,继续匹配结果如下:

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    P:

       

    a

    b

    a

    a

    b

    c

    a

    c

             

    j:

       

    1

    2

    3

    4

    5

    6

    7

    8

             

    f)。。。如此重复e)

    g)当i=8,j=6时,主串的与模式串当前字符不相等。情形如下:

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    P:

       

    a

    b

    a

    a

    b

    c

    a

    c

             

    j:

       

    1

    2

    3

    4

    5

    6

    7

    8

             

    此时,主串的指针i不回溯,那么模式串将向右滑动至什么位置呢?

    我们假设next[j]=k(k<j)。

    ①本回合,clip_image012[14]clip_image014[62]失配(i=8,j=6),而在失配之前有“部分匹配”,故有:clip_image016[14],又因为k<j,从而clip_image018[74]的部分串clip_image020[20]满足:clip_image022[74]

    ②下回合,因为要保证从模式串的k位置处字符开始比较而不回溯,那么必须保证模式串k位置之前的部分串clip_image024[20]满足:clip_image026[20],其中k取最大的可能值;

    由①式、②式右半边相等可知,k的取值满足下面的判别等式:clip_image028[74],其中k取该解集中的尽可能大值。考虑定义:next[j] = Max{k|1<k<j且clip_image028[75]}。

    上式也即:j位置之前的P尾串 = 1位置之后的P头串,如图中:clip_image030[14],k=3,反映了模式串在j位置之前的P尾串 与1位置之后的 P头串的重复程度。

    f)模式串向右滑动至位置next[j]=3,情形如下:

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    P:

             

    a

    b

    a

    a

    b

    c

    a

    c

       

    j:

             

    1

    2

    3

    4

    5

    6

    7

    8

       

    h)。。。后续匹配

    根据以上讨论,对k(next[j])考虑如下定义:

    (1)next[j] = 0(j=1);

    (2)next[j] = Max{k|1<k<j且clip_image028[76]}(j!=1且集合有解);

    (3)next[j] = 1(j!=1且集合无解);

    三、求取next[j]

    根据next[j]的定义可知,此函数的值取决于模式串的本身以及模式串的失配位置。此时把求解next[j]问题看成是一个模式串自匹配问题,即主串和模式串都是P,从主串P的clip_image032[14]开始与模式串P的clip_image034[20]开始进行匹配:

    当j=1时,由定义得:next[1]=0;

    当j!=1时,我们思路是:按照i的值由小及大,依次求next[2]、next[3]、...next[m],m为模式串的维数。现在假设已知next[j]=k,且next[t](t<j)均已求得。如果求得next[j+1]与next[j]的关系,那么所有的next函数值均可已被求出。

    此时,由next[j]的定义可知:clip_image028[77],且k为最大值,下面分两种情况讨论:

    (a)如果clip_image036[20],结合clip_image028[78]}可以得到clip_image038[20],又不存在k'>k满足该式,由next函数的定义可知:next[j+1]=k+1,也即:next[j+1]=next[j]+1。这个式子的意味着,该情况下主串字符指针(j+1)位置处的next[j+1]可以由当前j位置处的next[j]加1求得。(由于下标最小的next函数值next[1]=0是已知的,这使得按下标由小及大的顺序求解所有next函数值成为可能,这种情况对应着下面伪代码的 if 语句部分)

    (b)如果clip_image040[14],将模式串向右滑动至k'(k'=next[k]<k<j)位置,使得主串的clip_image014[63]字符与模式串的clip_image042[26]字符比较。

    ①如果此时clip_image044[14](k'<k),结合clip_image028[79],则有clip_image046[50],由next函数的定义该式等价于:next[j+1]=k'+1=next[k]+1(观察下标k<j,由于next[t](t<=j)已知,则一定可以求出next[j+1])。

    ②如果此时clip_image048[38],则将模式串继续向右滑动,直至clip_image014[64]和模式串的某个字符clip_image050[14]匹配成功,此时clip_image052[14](k_lucky<k),结合clip_image028[80],则有clip_image054[14],由next函数的定义该式等价于:next[j+1]=k_lucky+1=next[...next[k]...]+1(在几次连续的滑动过程中,每次迭代k'=next[k],k'<k<j恒成立,由于next[t](t<=j)可知已知,则一定可以求出next[j+1])。

    ①和②的讨论说明,无论经过多少次滑动,只要主串的clip_image014[65]最终与模式串clip_image050[15]字符匹配成功,则主串字符指针(j+1)位置处的next[j+1]一定可以由某一个next[t](t<j)加1求得。

    ③尽管向右滑动,很不幸找不到k'使得clip_image044[15],这相当于匹配过程中无解,此时由定义知next[j+1]=1。

    故伪代码如下:

    void get_next(SString P,  int &next[]){
        //求模式串P的next函数值并存入数组next中,i、j分别代表主串、模式串的下标
        i = 1; j = 0; next[1] = 0;
        while(i < P[0]){
            if( j ==0 || P[i] == P[j] ) { ++i; ++j; next[i] = j; }//每当自增i和j,得到一个新的next[i]
            else j = next[j];//模式串向右移动
        }
    }
    
    

    有以下模式串:

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

     

    P:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

     

    算法的前几次迭代过程列举如下:

    a)执行if,++i,++j,进入初始状态(i=2,j=1),next[2]=1:

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

     

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

     

    P:

     

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    j:

    0

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    b)执行else,模式串向右滑动(i=2,j=0):

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

       

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

       

    P:

       

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    j:

     

    0

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    c)j==0,执行if,++i,++j(i=3,j=1),next[3]=1:

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

       

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

       

    P:

       

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    j:

     

    0

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    d)clip_image056[26],执行if,++i,++j(i=4,j=2):

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

       

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

       

    P:

       

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    j:

     

    0

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    以此类推。。。

    四、改进的next函数值算法

    这样的改进已经是很不错了,但算法还可以改进,注意到下面的匹配情况:

    i

    1

    2

    3

    4

    5

    6

    7

    8

    9

     

    S

    a

    a

    a

    b

    a

    a

    a

    a

    b

     

    P

    a

    a

    a

    a

    b

             

    j

    1

    2

    3

    4

    5

             

    next[j]

    0

    1

    2

    3

    4

             

    nextval[j]

    0

    0

    0

    0

    4

             

    模式串P中的clip_image058[14]和主串S中的clip_image060[20]失配时,P向右滑动至next[4]=3位置,此时的比较还是会失配,然后P向右滑动至next[3]=2位置,此时的比较还是会失配,P向右滑动至next[2]=1位置。如果我们能够修正next[4]的值为1,使得出现类情况的时候,P能够一次性直接滑动到next[4]=1的位置,如此便可以消除这样的冗余比较。

    下面对next的函数值进行修正:如果我们事先知道,上一次主串模式串字符相等 clip_image062[14],经过++i,++j,再一次得到clip_image062[15],此时,i自增后的next[i]不取j,而是选取j的下一个状态next[j](由于j<i恒成立,next[j]的值均为已知的),这相当于我们在求next函数值的时候,把这些冗余的比较进行预处理,如此就可以消除模式串与主串之间这样的多余比较。

    稍加改进得到:

    void get_nextval(SString P,  int &nextval[]){
        //求模式串P的next函数修正值并存入数组nextval
        i = 1; j = 0; nextval[1] = 0;
        while(i < P[0]){
            if(j == 0 || P[i] == P[j]){
                ++i; ++j;
                if(P[i]! = P[j]) nextval[i] = j;//下次比较不相等,nextval[i]取j
                else nextval[i] = nextval[j];//下次比较仍然相等,nextval[i]取j的下一个nextval[j]
            }
            else j=nextval[j];
        }
    } 

    五、KMP算法

    求得next[j]的函数值之后,我们就可以根据KMP算法进行字符串匹配了。

    在匹配过程中,如果clip_image064[14],则i和j分别增1;否则,i不变,j移动到next[j]的位置继续比较,以此类推,直至下面两种可能:1)j退至某个next值(next[next[…next[j]...]])时字符比较相等,则指针各自增1,继续进行匹配;2)退到next[next[…next[j]...]]为0,此时需要将模式串向右滑动一个位置,即从主串的下一个字符clip_image066[8]起和模式串clip_image034[21]开始重新匹配。

    KMP算法伪代码如下:

    int Index_KMP(SString S, SString P, int pos){  
        //利用模式串P的next函数求P在主串S中第pos个字符之后的位置的KMP算法。
        //其中,P非空,1≤pos≤StrLength(S)。  
        i = pos; j = 1;  
        while(i <= S[0] && j <= P[0]){//S[0]以及P[0]代表字符串长  
            if(j == 0 || S[i] == P[j]) { ++i; ++j; }//继续比较后继字符
            else   j = next[j];//模式串象右滑动
        }  
        if(j > P[0])   return  i - P[0];//匹配成功  
        else   return  0;  
    }//Index_KMP

    以下主串和模式串的匹配过程如图所示:

    i:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    11

    12

    13

    14

    15

    16

    S:

    a

    c

    a

    b

    a

    a

    b

    a

    a

    b

    c

    a

    c

    a

    a

    P:

    a

    b

    a

    a

    b

    c

    a

    c

                 

    j:

    1

    2

    3

    4

    5

    6

    7

    8

                 

    clip_image067

    六、直接看出next[j]函数值(这部分有待考证)

    根据以上next[j]的定义:

    (1)next[j] = 0(j=1);

    (2)next[j] = Max{k|1<k<j且clip_image028[81]}(j!=1且集合有解);

    (3)next[j] = 1(j!=1且集合无解);

    可以直接看出模式串的next函数值。这主要遵循2条规则:

    a)如果上一个next[j]!=1,按照next[j] = Max{k|1<k<j且clip_image028[82]}的规则求解下一个next[j+1]的值;

    b)如果上一个next[j]=1,由next[j]的求解算法知,此时主串的第j位与模式串的第1位对齐,比较这两位:如果不等,则主串的第j+1位与模式串的第1位再对齐,故next[j+1]=1;如果相等,则next[j+1]=Max{k|1<k<j且clip_image028[83]}

    举例如下:

    P

    a

    b

    a

    a

    b

    a

    a

    a

    b

    a

    j

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    next[j]

    0

    1

    1

    2

    2

    3

    4

    5

    2

    3

    由定义知next[1]=0;

    由j!=1且集合无解知next[2]=1;

    由于next[2]=1,此时i=2,j=1,令这两个字符对齐,发现这两个字符不相等,则next[3]=1;

    由于next[3]=1,此时i=3,j=1,令这两个字符对齐,发现这两个字符相等,根据next[j+1]=Max{k|1<k<j且clip_image028[84]}=2,故next[4]=2;

    P(1之后串头)

    a

    b

    a

    a

    b

    a

    a

    a

    b

    a

    P(j之前串尾)

    a

    b

    a

    a

    b

    a

    a

    a

    b

    a

    j

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    next[j]

         

    2

               

    由于有1个匹配,故next[5]=2;

    P(1之后串头)

    a

    b

    a

    a

    b

    a

    a

    a

    b

    a

    P(j之前串尾)

    a

    b

    a

    a

    b

    a

    a

    a

    b

    a

    j

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    next[j]

           

    2

             

    由于有2个匹配,故next[6]=3;

    P(1之后串头)

    a

    b

    a

    a

    b

    a

    a

    a

    b

    a

    P(j之前串尾)

    a

    b

    a

    a

    b

    a

    a

    a

    b

    a

    j

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    next[j]

             

    3

           

    。。。后续匹配

    这个文章思路是我理解KMP的思路,内容可能显得比较啰嗦,但是很具体,希望对大家有所帮助。

    第一次写文章,还请大家多多提意见。

    参考文献:《数据结构》,严蔚敏著

  • 相关阅读:
    HDU 1251 统计难题(字典树模板题)
    POJ 1182 食物链(带权并查集)
    FJUT 2351 T^T的图论(并查集)
    10.QT程序框架与connect
    9.正则表达式
    8.QList QMap QVariant
    7.treeview
    6.图形化列表查询显示
    5.listview(QStringList QStringListModel)
    4.QList
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/2291417.html
Copyright © 2011-2022 走看看