KMP的自我理解

Posted tan90丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KMP的自我理解相关的知识,希望对你有一定的参考价值。

KMP算法的核心思想为,当文本串与模式串在某一位置发生失配时,利用已经匹配部分的信息,让模式串迅速向后移动,以完成快速匹配。

很重要的一点

模式串快速后移多少个单位,或者说失配后,文本串应该继续和模式串中的哪个字符继续比对,也就是常说的next[j]的值,这个值是只与模式串有关,而与文本串是无关的。

为什么只与模式串有关

注意:下面讨论时,P[0,j)为左闭右开区间,包括下标为0不包括下标为j的字符,而P[0,j]则表示闭区间。

设文本串T和模式串S,假设在T[i]与P[j]处发生失配时,此时,我们已经掌握了文本串T[i-j,j)这部分子串的全部信息,KMP算法就是要利用这部分已知信息,快速确定T[i]应该继续和模式串中的哪个字符进行比对。

值得庆幸的是,这部分信息和P[0,j)是完全一致的。因为在比对T[i]与P[j]时,说明T[i-j,i)和P[0,j)已经是匹配成功的,才有继续往后比对的必要。

因此,我们可以根据模式串,提前计算出P[j]与文本串某个字符发生失配时,继续使用模式串哪个位置的字符与文本串进行比对,这个值就是next[j]。因此,KMP算法的核心就是提前计算出在模式串某位置j发生失配时,应该跳到哪个位置继续比对,这些位置组合起来,就是next数组。

注意:next[j]的值表示,当模式串P{j]与文本串T[i]发生失配时,使用P[next[j]]代替P[j]继续和T[i]进行下一次比对。

next数组头脑风暴
  1. T与P进行匹配,T[i]与P[j]相等时,和蛮力算法一致,二者一起后移,继续比对T[i+1]和P[j]。
  2. 当T[i]与P[j]发生失配时,则利用next数组获得下一个应该比对的位置,继续和T[i]进行比对即可。

    T[i]与P[j]失配时,拿到next[j],让P[next[j]]继续和T[i]进行比对,当然如果还是失配,那么继续和P[next[next[j]]]进行比对,一直这样迭代下去

    按上述思路一直迭代下去,最终必然会出现两种情况:

    2.1 j一直往前迭代,找到了某个位置j‘,使得P[j‘]=T[i],那么此时可以确定T[i-j‘, i]和P[0,j‘]已经匹配,二者一起后移,继续比对T[i+1]和P[j‘+1],这又是新一轮的比对

    2.2 j一直往前迭代,都没有找到和T[i]一致的字符,此时,j‘一定会越界(程序中一般设置为-1),这种情况,说明找不到和T[i]对应的字符,应该跳过T[i],让T[i+1]和P[0]开始新一轮的比对

    上述第二种情况,和T[i]与P[0]比对后不一致是类似的,此时,首字母不匹配,直接抛弃T[i],让T[i+1]和P[0]开始新一轮的比对

利用假想哨兵统一上述两种情况

可以假想在模式串的下标为-1处有一个通配哨兵,这个哨兵与任何字符都是匹配的。当T[i]与P[0]失配时,继续往前,让T[i]与P[-1]进行比对,此时,必然比对成功,按照2.1的思路,即找到j‘=-1,这样,二者一起后移,继续比对T[i+1]和P[j‘+1],即T[i+1]和P[0],这样,便可以将2。2统一到和2.1一致的思路中

注意:利用哨兵时,next[0]必须为-1,这样,在越界时,使用2.1中的思路,找到的j‘是-1,,下轮比对才会恰巧是T[i+1]和P[0],因此,next[0]=-1也是next数组构造的初始条件

已知next数组情况下,进行KMP模式匹配的代码
/*
    KMP匹配
    @param: T 文本串
    @param: P 模式串
    @return: 失败返回-1,成功则返回模式串在文本串的起始下标 
*/
int kmpMatch(const char* T, const char* P){
    
    int len = strlen(T);
    int patternLen = strlen(P);
    
    //生成next数组 
    int* next = new int[patternLen];
    getNext(next, P);
    
    int i = 0, j = 0;
    //i<len,表示还能继续匹配
    //j<patternLen,表示还没匹配成功 
    while(i < len && j < patternLen){
        //j<0相当于匹配到通配哨兵
        //T[i] == P[j]则表示当前比对通过
        //这两种情况都为比对通过,文本串和模式串一起后移,继续比对T[i+1]和T[j‘+1] 
        if(j < 0 || T[i] == P[j]){
            ++i;
            ++j;
        }else{
            //按照2.2的思路,若找不到合适的j‘,则一直利用next数组往前迭代,直到找到相等的或者匹配到哨兵(哨兵是通配的) 
            j = next[j];
        }
    }
    delete[] next;
    //由于串长为patternLen,那么比对成功的起始位置不可能超过Len-patternLen 
    if(i-j > len-patternLen){
        return -1;
    }
    return i-j;
}

next数组的构造

再次强调,next数组的构造只和模式串有关

next[j]的值表示P[j]与T[i]失配后,使用P[nex[j]
继续和T[i]进行新一轮的比对

由于next数组的构造理解起来较为困难,因此先通过一个例子找一下规律:

设模式串P="aabbccaabbd",在下标为10的字母d处发生失配时,此时我们能掌握前缀P[0,10)信息,分析这个前缀,可以发现它的前缀"aabb"和它的后缀"aabb"是完全一致的,那么我们可以直接让P[4]替代P[10]继续下一轮的比对,在这个例子中,next[10]=4

仔细思考和分析,可以总结出这个规律:在P[j]处发生失配时,分析该位置之前的子串,找到它的最长公共前后缀,那么这部分公共前后缀的信息是可以不用比对的,可以让前缀的下一个字符替代P[j]和T[i]进行下一轮的比对

再注意一个微妙的地方:由于字符串的下标从0开始,因此最长公共前后缀的长度恰好就是下一个应该比对的字符的位置,因为长度正好等于最后一个字符位置+1

例如:前面的例子中,对于模式串P="aabbccaabbd",在P[10]处发生失配时,P[0,10)的最长公共前后缀为"aabb",其长度为4,而前缀"aabb"的最后一个字符b在字符串中的位置为3,那么跳过前缀,下一个比对的位置正好应该是3+1=4,其恰好也为字符串的长度4。

但是我们不可能对于模式串的每个位置,都直接去计算最长公前后缀,而是利用模式串自相似的特性,快速构造next数组

注意构造next数组的过程和KMP使用next的过程是类似的,构造模式串的过程就像是利用已经求的的前半部分的next数组,让前缀去匹配后缀,求出最新的next[j],一直这样往后迭代

因此,我们构造next数组都是基于已知的next[0,j],求next[j+1]的迭代过程:

  • 当P[j]=P[next[j]时,那么P[0,j]的最长公共前后缀会在原来的基础上+1,即next[j+1] = next[j] + 1
  • 当P[j]!=P[next[j]]时,设next[j]=j‘,可以理解为这是一次前缀P[0,j‘]和后缀P[j-j‘, j]失配的过程此时P[j-j‘,j]相当于文本串,那么,按照前面的思路,应该让j‘继续王钱迭代,知道找到P[j‘]=P[j],此时按照和上面相同的处理思路即可

技术分享图片

next数组构造的模拟

设模式串P="aabbccaabbd",len=11,下标为0-10,设变量i为已求出next值的最后一个下标,而j即为前缀P[0,i)的最长公共前后缀长度,也就是在P[i]处失配时替换的位置,即j=next[i]

  1. 初始化:i=0,j=-1,next[0]=-1,表示首字母失配时,和哨兵比对

    注意:i为已经求出next值的最后一个位置,并利用next[0,i]的信息求解next[i+1],因此i<len-1. i+1最大为len-1。

  2. i=0,j=-1,欲求next[1]:
    1. 判断p[i]==p[j],是,则next[++i]=++j
    2. 此时next[1] = 0,i = 1,j = 0
  3. i=1,j=0,欲求next[2]:
    1. 判断p[i]==p[j],是,则nezt[++i]=++j
    2. 此时next[2] = 1,i = 2,j = 1
  4. i=2,j=1,欲求next[3]:
    1. 判断p[2]==p[1],否,失配,则j=next[j]=0
    2. 判断p[2]==p[0],否,失配,则j=next[j]=-1
    3. 判断p[2]==p[-1],是,匹配成功,二者一起后移,next[++i]=next[++j]
    4. 此时next[3]=0,i = 3,j= 0

    其实1-3是递归求解可以匹配的j的过程,其过程和已知next数组进行KMP模式匹配的过程是一致的,只不错这里前缀充当模式串,后缀充当文本串

  5. i=3,j=0,欲求next[4]:
    1. 判断P[3]==P[0],否,递归求解j=-1
    2. next[++i] = ++j
    3. 此时,next[4]=0,i=4,j=0
  6. i=4,j=0,欲求next[5]:
    1. 判断P[4]==P[0],否,递归求解得j=-1
    2. next[++i] = ++j
    3. 此时,next[5]=0,i=5,j=0,
  7. i=5,j=0,欲求next[6]:
    1. 判断P[5]==P[0],否,递归求解得j=-1
    2. next[++i]=next[++j]
    3. 此时,next[6]=0,i=6,j=0
  8. i=6,j=0,欲求next[7]:
    1. 判断p[6]==p[0],是,二者一起后移
    2. next[++i]=++j;
    3. next[7]=1,i=7,j=1
  9. i=7,j=1,欲求next[8]:

    1. 判断P[7]==p[1],是,二者一起后移
    2. 则next[++i]=++j
    3. 此时,next[8]=2,i=8,j=2
  10. i=8,j=2,欲求next[9]:
    1. 判断P[8]==P[2],是,则二者一起后移
    2. next[++i]=next[++j]
    3. 此时,next[9]=3,i=9,j=3
  11. i=9,j=3,欲求next[10]:
    1. 判断P[9]==P[3],是,二者一起后移
    2. next[++i]=++j;
    3. 此时,next[10]=4,i=10,j=4,next数组求解完毕

其实next数组的迭代求解其实是利用了这样一个规律:当前仅当P[j]==P[next[j]]时, 这P[0,j+1]的最长公共前后前缀包含了P[0,j]的最长公共前后缀,且长度为P[0,j]的最长公共前后缀+1;若是不同,则说明P[0,j+1]的最长公共前后缀不包括P[0,j]的最长公共前后最,但可能包括P[0,next[j]]的最长公共前后缀,因此要一直往前找,知道找到相等的或者匹配到哨兵为止。

构造next数组C++版本
/*
    构造next数组
*/
void getNext(int* next, const char* P){
    next[0] = -1;
    int len = strlen(P);
    int i = 0;
    int j = -1;
    while(i < len-1){
        //匹配到哨兵或者相等 
        if(j < 0 || P[i]==P[j]){
            next[++i] = ++j;
        }else{
            //递归往前搜索 
            j = next[j];
        }
    }
}
KMP算法的优化

上述的next数组的构造方式其实还是有可优化的空间的,我们来看一个极端的例子:

设模式串T="aa",则next[0]=-1,i=0,j=-1,判断P[0]==P[-1],是,那么next[++i]=++j,即next[1]=-1+1=0

问题就在于这个next[++i]=++j,next[1]=-1+1=0

  1. 首先,next[1]等于0表示,当P[1]与文本失配时,用P[0]替代P[1]继续和文本串进行比对,但毫无疑问,这次比对必然会失配,因为P[0]和P[1]是相等的,这样我们做了一次多余的比对后,让程序继续j=next[0]=-1;

  2. 由于我们在构造next数组时,求的最长公共前后缀的长度后,便直接将这个值作为下一个比对的位置(next[++i]=++j,这里的++j其实就是最长公共前后缀的长度),却不管这个位置上的字符是否已经和当前确定失配的字符是一样的,因此,才会多出来这些多余的比较

那前面的T="aabbccaabbd"来说,其原有的next数组为[-1,0,1,0,0,0,0,1,2,3,4],假设我们在第9个字符b处发生失配,求的P[0,8]的最长公共前后缀为3,我们便直接另next[9]=3,却不管P[3]是否和P[9]相等,而在这个例子中P[3]恰好等于P[9],P[9]失配则毫无疑问P[3]一定会失配,程序继续使用next[3]的值代替P[3]继续往前搜索。

但是,我们如果在构造next的过程中,提前判断P[next[j]]是否等于P[j],若相等,则跳过next[j]位置的比对,直接使用next[next[j]]处的值替代next[j],这样,便可以避免此次比对

如上述例子:j=9,原next[9]=3,由于P[3]==P[9],直接跳过P[3],使用next[3]代替3,这样便可以跳过很多不必要的比对

next数组构造的优化
/*
    构造next数组
*/
void getFastNext(int* next, const char* P){
    next[0] = -1;
    int len = strlen(P);
    int i = 0;
    int j = -1;
    while(i < len-1){
        if(j < 0 || P[i]==P[j]){
            ++i; 
            ++j;
            if(P[i]!=P[j]){
                //若和当前已经失配的字符不相等,直接使用最长公共自前缀赋值 
                next[i] = j;
            }else{
                //若和当前已经失配的字符相等,则跳过本次比对,直接往前 
                next[i] = next[j];
            }
        }else{
            j = next[j];
        }
    }
}


以上是关于KMP的自我理解的主要内容,如果未能解决你的问题,请参考以下文章

KMP算法的基本操作和自我理解

kmp算法的个人理解

POJ-2752(KMP算法+前缀数组的应用)

Luogu_P3435 [POI2006]OKR-Periods of Words KMP

KMP算法的理解和代码实现

kmp算法的理解