深度理解KMP算法

       KMP算法是一种改进的字符串匹配算法,KMP算法的关键是匹配即使发生失败,主串也不回退,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。

       这是KMP的一般定义,但是实际我们对于KMP算法的实现总是有许多的不解,即使记下了它的实现过程,也还是不理解,这种没有基于理解的死记硬背总是让我们过不了多久就会忘记。前两天在不愿意透漏姓名的刘先生的讲解下,让我对于KMP算法有了完整的推导与真正深入的理解,因此在这里整理我对于KMP算法的理解,便于日后复习。


KMP算法的实现过程

       首先我们设主串(下简称为T)为:a b a c a a b a c a b a c a b a a b b,模式串(下简称为W)为:a b a c a b。

        为了能够在每一次匹配失败时找到下一次匹配最合适的匹配位置,我们需要记录模式串中的一些信息,我们用一个数组来记录这些信息,给这个数组起名为NEXT,也就是我们常说的NEXT数组。

       那么这个NEXT数组中都存了些什么呢?实际上,NEXT数组中储存的是模式串中每一个元素之前的所有元素的最大前后缀数,即前缀等于后缀的最大长度,并且我们规定 NEXT[ 0 ] = -1,NEXT[ 1 ] = 0。关于NEXT数组的求法我会放在后面讲解。

       求出W的NEXT数组如下:

      现在我们开始匹配,令 i = 0,j = 0,当T[ i ] == W[ j ]时,i++,j++,当i = j = 5时,我们可以看到W[ i ] != T[ j ],  即匹配失败

       此时让我们拿出T[ i - j ]到T[ i - 1 ]的元素(下称为串str1)和W[ 0 ]到W[ j - 1 ]的元素(下称为串str2),会发现str1 == str2,因为在 i 和 j 之前的匹配都是成功的。从NEXT[ j ]我们可以知道str2的最大前后缀的长度为1,而str1 == str2,因此它str2的最大前后缀的长度也是1,又因为 str1的前缀 == str1的后缀,str2的前缀 == str2的后缀,且 str1 == str2,可得 str2的前缀 == str1的后缀,这样我们就只需要从str1后缀后的元素开始与str2前缀后的元素开始重新匹配,即 j 跳到NEXT[ j ]的位置与 T[ i ]重新匹配即可,不需要再像以前的暴力算法中需要让 i 跳回T数组的最前面开始重新匹配,从而实现快速匹配。

        重新匹配后,依然有T[ i ] != W[ j ],那么继续来看NEXT[ j ],此时NEXT[ j ] == 0,表示在元素 j 前的所有元素中最大前后缀的长度为0,我们需要将W元素从头开始与T[ i ]匹配,即 j 跳到NEXT[ j ] = 0的地方开始与T[ i ]进行下一次匹配。当NEXT[ j ] = -1时就说明当前的T[ i ]与W数组中的第一个元素都不匹配,此时 i++,从T数组中 i 的下一个元素开始匹配即可。

        再来整理一下,现在我们知道,NEXT数组存在的意义就是为了告诉我们匹配失败后的下一次匹配应该从哪里开始。如下图中,当我们在红色处匹配失败时,由于前面的匹配都是成功的,因此 str1 == str2,根据最大前后缀的性质,我们可以得到 str1的后缀 == str2的前缀,即不用再次匹配,我们也可以知道str1的后缀部分与str2的前缀部分是相等的,只需要将str1的后缀之后的部分与str2前缀之后的部分再次匹配,即从下图中蓝色的部分再次匹配即可。

       让我们再通过一个例子来理解一下。

       设主串T为:a a a a a a a a a a a b,模式串W为:a a a a b,首先我们先计算出NEXT数组如下图:

       可以看到,这样的一组字符串匹配,如果按照暴力算法直接去匹配,效率是非常非常低的。现在我用KMP算法来进行匹配,可以看到,当 i = j = 4 时,匹配失败,

       同样让我们拿出 T[ i - j ] 到 T[ i - 1 ] 的元素str1和 W[ 0 ] 到 W[ j - 1 ] 的元素str2,通过NEXT[ j ]知道str2的最大前后缀长度为3,因为str1与str2匹配成功,即 str1 == str2,所以str2的最大前后缀的长度也为3,我们再次根据最大前后缀的定义可得,str1的后缀 == str2的前缀,此时我们让 j 跳至NEXT[ j ]对应的下标位置,然后再次匹配T[ i ]与W[ j ]即可。

       当我们在主串中找到与模式串匹配的位置时,只需要返回 i - j,即是在主串中与模式串匹配的第一个位置。

求解NEXT数组

       现在让我们来看看NEXT数组是怎么求出来的。

       首先我们规定NEXT[ 0 ] = -1,NEXT[ 1 ] = 0,这是因为前缀的定义中规定前缀不能包含最后一个元素,后缀规定不能包含最后一个元素,而第一个元素前没有元素,不存在前缀后缀,第二个元素前只有一个元素,也不符合前缀后缀的定义规定,因此我们只能人为的去规定这两个位置的数值。

       现假设有一个模式串W为:a b c a b a b c d,令NEXT[0] = -1,NEXT[1] = 0,定义一个 jmp 指针指向W[0],用来记录当前最大前后缀中前缀后的第一个元素,定义一个 j 指针指向 W[2],用来指向当前需要填入的 NEXT[ j ] 位置,也是当前最大前后缀中后缀后的第一个元素。

       现在我们来比较 W[ jmp ] 和 W[ j-1 ],此时 W[ jmp ] != W[ j-1 ],说明 j 前的序列中最大前后缀长度为0,即 NEXT[ j ] = 0,j++,继续比较,发现 W[ jmp ] != W[ j-1 ],那么 NEXT[ j ] = 0,j++,当 j == 4时,W[ jmp ] == W[ j-1 ],那么说明 W[ j ] 前序列的最大前后缀长度为 1,即 NEXT[ j ] = 1,此时最大前后缀的长度为 1,jmp++,指向当前最大前后缀中前缀后的第一个元素,j++,此时 j 指向的就是当前最大前后缀中后缀后的第一个元素;接着我们再比较 W[ jmp ] 和 W[ j-1 ],W[ jmp ] == W[ j-1 ],那么jmp++,即 NEXT[ j ] = 2,再让 j++,继续比较,此时会发现 W[ jmp ] != W[ j-1 ],此时我们就需要让jmp跳到NEXT[ jmp ]的数组下标位置,即 jmp = NEXT[ jmp ],然后再比较 W[ jmp ] 和 W[ j-1 ],看看是否匹配。这是计算NEXT数组中最重要的一步,也是最难理解的地方,下面让我们来理解一下这里为什么要这样做。

       在一个模式串W中,当我们在x1、y位置匹配失败时,x1、y前面有已经匹配成功的前后缀str1和str2,这个最大前后缀的长度记录在 y 位置所对应的NEXT数组位置,即NEXT[ j ]位置。而在str1中,可能还有已经匹配成功的前后缀str3和str4,这个最大前后缀的长度是记录在 x1 位置对应的NEXT数组里的,即NEXT[ jmp ]位置。

       根据前后缀的定义,我们可以得到 str3 == str4,str5 == str6,并且又有 str1 == str2,所以可以得到 str3 == str6,那么如果 str3 后的 x2 与 str6 后的 y 匹配成功,我们就可以的到一个较短的最大前后缀了,而元素 x2 的位置信息是记录在 x1 位置对应的NEXT数组中,所以这就是我们要将 jmp 跳转至 NEXT[ jmp ] 的位置来继续和 j 比较的原因了。

       现在我们接着来看上面的例子,当W[ jmp ] != W[ j-1 ]时,令 jmp = NEXT[ jmp ],即 jmp = 0,再次比较W[ jmp ]和W[ j-1 ],此时有 W[ jmp ] == W[ j-1 ],则 NEXT[ j ] = ++jmp,即 NEXT[ j ] = 1,j++......以此类推,直到求出整个NEXT数组,

       这就是整个KMP算法的求解思路,下面我们来看代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

//求解next数组
int* generate_next(char* needle)
{
    int* next = (int*)malloc(4*strlen(needle));
    *next = -1;
    *(next + 1) = 0;
    int l2 = strlen(needle);
    int jmp = 0;
    int i = 2;
    while(i < l2){
        //匹配成功
        if(*(needle+i-1) == *(needle+jmp)){
            *(next + i++) = ++jmp;
        }
        //匹配失败,jmp跳转至小区域匹配成功的最大前缀的后一个元素
        else if(jmp > 0){
            jmp = *(next + jmp);
        }
        //jmp = 0,目前没有匹配成功的最大前后缀
        else{
            *(next + i++) = 0;
        }
    }
    return next;
}

//haystack为主串,needle为模式串
int strStr(char* haystack, char* needle)
{
    int l1 = strlen(haystack);
    int l2 = strlen(needle);
    if(l1 < l2){
        return -1;
    }
    if(l2 == 0){
        return 0;
    }
    
    int i = 0;
    int j = 0;
    int *next = generate_next(needle);
    while(i < l1 && j < l2){
        //匹配成功
        if(*(haystack+i) == *(needle+j)){
            i++;
            j++;
        }
        //模式串已跳至第一个并且匹配失败,主串++
        else if(*(next+j) == -1){
            i++;
        }
        //匹配失败,跳至当前最大前缀的后一个元素
        else{
            j = *(next + j);
        }
    }
	//next数组的空间是我们手动开辟的,使用完要手动释放
    free(next);	
    
	//当 j 走至模式串结尾时表示在主串中匹配成功,返回 i-j,即主串中匹配成功的位置
	//否则匹配失败,返回 -1
    return j = j == l2 ? i - j : -1;
}

 

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值