KMP算法使用

本文详细介绍了KMP算法在模式串匹配中的应用,重点讲解了next数组的三种求解方法:前后缀法、公式法和代码法。通过实例解析了next数组的计算过程,并阐述了KMP算法的匹配步骤,帮助理解如何消除回溯提高匹配效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

KMP算法使用

前言

在考研备考时,大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在经过一天的学习之后kmp算法之后,把一些思考的结果整理出来,与大家一起探讨。

用途

KMP算法时用来求解模式串匹配的问题,也就是Index(S,P)函数。
给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。

前提引入

在求解模式串匹配问题时,我们之前用到的方法就是暴力匹配,这种算法在于主串需要不断回溯,因此算法的时间复杂度为O(m+n);
而KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的。而消除回溯的重点在于利用next数组。

正文

所以本文的重点是如何求解next数组以及如何利用next数组进行字符串匹配。
【说明:本文不涉及kmp算法的原理介绍,需要的同学可以去这篇博客,里面介绍了原理,并且用动画进行演示。(算法)通俗易懂的字符串匹配KMP算法及求next值算法_Sirm23333-CSDN博客_kmp算法next计算方法 https://blog.csdn.net/qq_37969433/article/details/82947411】

求解next数组

我们可以从以下角度出发。

1、前后缀法

那么,我们就要理解什么是前缀,什么是后缀。

前缀是指除最后一个字符外,字符串所有的头部子串;后缀是指除第一个字符外,字符串所有尾部子串。Eg:ababaa的前缀为:a、ab、aba、abab、ababa;而它的后缀为:a、aa、baa、abaa、babaa;
Next表示的含义是最长相等匹配前后缀的长度+1。

因此,求解ababaa的next数组时,我们可以按照如下方法

模式Sababaa
编号j123456
next[j]01

对于next[1],规定直接为0
对于next[2],此时模式串s为a【不含当前字符】,a的前缀为空集,后缀为空集,交集为空,所以最长相等匹配前后缀为0,所以next[2]=0+1=1;
对于next[3],此时模式串s为ab【不含当前字符】,a的前缀为a,后缀为b,交集为a,所以最长相等匹配前后缀为1,所以next[3]=0+1=1;
对于next[4],此时模式串s为aba【不含当前字符】,a的前缀为a、ab,后缀为ba、a,交集为a,所以最长相等匹配前后缀为1,所以next[4]=1+1=2;
对于next[5],此时模式串s为abab【不含当前字符】,a的前缀为a、ab、aba,后缀为bab、ab、b,交集为ab,所以最长相等匹配前后缀为2,所以next[5]=2+1=3;
对于next[6],此时模式串s为ababa【不含当前字符】,a的前缀为a、ab、aba、abab,后缀为baba、aba、ba、a,交集为a、aba,所以最长相等匹配前后缀为3,所以next[6]=3+1=4;

模式Sababa
子串ababa
模式Sababa
子串ababa

综上,我们求出ababaa的next数组

模式Sababaa
编号j123456
next[j]011234

个人感悟:上述方法最容易记住,也容易求解。
需要注意的是,求解next[j]时为不含第j位的字符串;
最长匹配前后缀长度+1

2、公式法

next[j] = 0 j=1
next[j] = Max{k |1<k<j&’p1p2…pk-1’=’pj-k+1 pj-k+2…pj-1} 当集合不空时
next[j] = 1 当集合为空时

这个公式是王道参考书上给的公式。本文不涉及这个公式是怎么出来的,只是教会你如何记住和使用它。

首先第一行 next[1]=0;这个直接记住就行,任何情况都能用。

解释一下第二行,max。
比如ababaa求解next[6]时,首先根据1<k<j,可知k的备选项有2、3、4、5
k=5 p1p2p3p4=abab p2p3p4p5=baba 两者不等
k=4 p1p2p3=aba p3p4p5=aba两者相等
k=3 p1p2=ab p4p5=ba两者不等
k=2 p1=a p5=a两者相等
所以next[6]=max{2,4}=4;
其实观察上述公式,我们求解时比较了p1、p1p2、p1p2p3、p1p2p3p4这些本质上就是前后缀的方法。只是这里利用了公式化的方法进行了表述。’p1p2…pk-1’=’pj-k+1 pj-k+2…pj-1实际上就是从p1开始,一直到pk-1;从pk-1开始,往前补充字符,直到字符个数和前面一样。【总之就是前面的p1开始pk-1结束,后面的只需记住pj-1结束就行,那个太复杂的pj-k+1不用记。】以next[4]为例,先根据1<k<j,可知k的备选项只有2、3。当k=2时;因为j=4我们比较p1和p3,即a=a;当k=3时,我们比较p1p2和p2p3,即ab≠ba,所以k=2符合条件。即next[4]=2;
补充一点:在利用跟这个方法进行计算时,其实不必求出所有情况下的k,判断是否相等。比如上例,当我们知道k=4相等时,就不用再继续匹配了,直接求出next=4;这也就是为什么我们是从k=5开始递减,而不是从2开始递增。

解释公式中的第三行,其他情况:
其他情况就是集合为空时。比如求解next[3]时首先根据1<k<j,可知k的备选项只有2。当k=2时,p1=a,p2=b 两者不等。所以next[3]=max{k|空集},所以next[j]=1;

3、代码法

以上方法在我们手动时都很方便,但是如果要用代码来实现,那么就有一定的难度。
首先有公式可知next[1]=0;
现在我们来讨论在知道前j个元素的next的情况下,如何求解next[j+1]

1、next[j+1]的最大值为next[j]+1。
因为:假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。
2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]

我们以下面这个例子为例进行分析。求模式串ababaacba的next数组。为了重点体现上面的分析过程,我以求解next6和next7和next8为例

模式ababaacb
j12345678
next[j]01123421

求解next[6]时
对于next[5]=3,我们可以知道最长匹配长度为2,也就是说p1p2=p3p4,而p1p2p3≠p2p3p4。所以当j=5时,我们只需比较pj和pnext[j]是否相等,即p5=a,p3=a;两者相等,所以p1p2p3=p3p4p5,(也一定有p1p2p3p4≠p2p3p4p5,next[6]不可能更大。这也就解释了next[j+1]的最大值为next[j]+1。)最长前后缀匹配长度为3,所以next[6]=3+1=4。

求解next[7]时
对于next[6]=4,我们可以知道最长匹配长度为3,也就是说p1p2p3=p3p4p5,而p1p2p3p4≠p2p3p4p5,所以当j=6时,我们只需比较pj和pnext[j]是否相等,即p6=a,p4=b;两者不相等,所以p1p2p3p4≠p3p4p5p6,最长前后缀匹配长度小于4。那么我们需要去分析是否存在更短的前后缀匹配。即分析p1p2p3是否等于p4p5p6、p1p2是否等于p5p6、p1是否等于p6。

——————————————————————
在匹配之前,我们补充一个例子abacabbdabacabac

模式串abacabadabacabace
j1234567891011121314151617
Next[j]0112123412345678

求解next[17]时,我们知道next[16]=8,也就是p1p2p3p4p5p6p7=p9p10p11p12p13p14p15;而比较p8和p16发现不等,所以最长匹配长度小于8。所以我们需要比较p1,p1p2,…,p1p2p3p4p5p6p7是否对应和p16,p15p16,…,p10p11p12p13p14p15p16是否相等,【先比较长的】但我们真的需要一个一个比吗?或者说我们都有必要比吗?
事实上我们已经知道p1p2p3p4p5p6p7=p9p10p11p12p13p14p15了而如果p10p11p12p13p14p15p16和其相等的话,那么一定有p9p10p11p12p13p14=p10p11p12p13p14p15【不考虑新加进来的p16】,也就是对应到前面的p1p2p3p4p5p6=p2p3p4p5p6p7,那么按照这样的情况,应该有next[8]=7;而实际上,只有next[8]=4,即p1p2p3=p5p6p7=p13p14p15,p1p2p3p4≠p4p5p6p7所以我们不必计算也知p1p2p3p4p5p6p7≠p10p11p12p13p14p15p16。同样的道理,我们也不必比较其他。
所以现在问题是已知p1p2p3=p13p14p15,那么如果p4=p16那么有p1p2p3p4=p13p14p15p16,最长匹配长度为4,next=5。
总结,我们求解next[17],也就是j=16时,因为next[16]的值是8,所以我们让p16和p8比较【如果相等,那么next[17]=next[16]+1】,如果不等,因为next[8]的值是4,所以我们让p16和p4比较,如果相等,那么next[17]=next[8]+1,【如果不等,继续比较p16和pnext[4],直到相等或找到首位】
————————————————————————————————

回到本题继续求解next7,我们已经知道p6≠p4,所以我们去比较p6与pnext[4],即p6与p2,发现不等,所以我们去比较p6与pnext[2],即p6与p1,发现相等,所以next[6]=next[2]+1=2;

求解next[8]
同样用上面的方法,因为next[7]=2,我们比较p7和p2比较,发现不等,所以我们去比较p7与pnext[2],即p7与p1,发现不等,所以我们去比较p6与pnext[1]即p6和p0(但p0不存在)这种情况下意味着,我们的第一个字符都无法与p7相匹配,所以最长匹配前后缀长度为0,所以next值为0+1=1。
至此,我们以next[6]、next[7] 、next[8]为例,讲解了一次找到、多次找到和找不到的情形,讲清楚了在不同情况下的代码的跳转逻辑,我们回过头再来阅读这段代码:

int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
    next[1] = 0;
    int i = 1,j = 0;
    while(i<=cLen){//遍历计算机每一个next
        if(j==0||ch[i]==ch[j])
 next[++i] = ++j;
        else j = next[j];
    }
}

只有当j=0,找不到或者ch[i]==ch[j](一次或多次匹配成功)程序退出。并让i++,继续求解下一个数的next;

总结:
以上就是从前后缀法、公式法和代码法三个角度对next数组的求解和简要分析。从我个人的角度来说,如果只是手算,还是以第一种方法为主。
补充一点:next[1]=0;next[2]=1直接写,任何情况。
————————————————————————————————————————————
———————分割线———————
————————————————————————————————————————————

KMP算法的匹配过程

下面进入本文的第二点,也就是在求解完一个模式串的next数组之后,如何利用next数组,写出KMP算法的匹配过程。
与next数组的求解相比,KMP的匹配算法相对要简单很多,它在形式上与简单地模式匹配算法很相似,不同之处在于当匹配算法失效时,指针i不变,指针j回退到next[j]的位置重新进行比较,并且当指针j=0时,指针i和j同时+1。即若主串的第i个位置和
模式串第一个字符不等,则应从第i+1个位置开始匹配。
具体代码如下:

{
    int i=1;j=1;
    while(i<S.length&&j<T.length){
        if(j==0||S.ch[i]==T.ch[j]){
            ++i;
            ++j;
        }else
        j=next[j];
    }
    if(j>T.length)
    return i-T.length;
    else return 0;
}

我们还是以最开始的模式串ababaa分析。在字符串S=’abababacababaa’找到子串。
解题:首先,我们求出ababaa的next数组为

sababaa
编号j123456
Next[j]011234

第一趟:从

主串abababacababaa
子串a

主串abababacababaa
子串ababaa

发生失配时,i=6,j=6;因为next[6]=4,所以我们让i=6和j=4对齐,继续匹配。也就是第二趟

主串abababacababaa
子串abab

主串abababacababaa
子串ababaa

发生失配时,i=8,j=6;因为next[6]=4,所以我们让i=8和j=4对齐,继续匹配。
也就是第三趟

主串abababacababaa
子串ab

主串abababacababaa
子串abab

发生失配时,i=10,j=4;因为next[4]=2,所以我们让i=10和j=2对齐,继续匹配。
也就是第四趟

主串abababacababaa
子串ab

发生失配时,i=10,j=2;因为next[2]=1,所以我们让i=10和j=1对齐,继续匹配。
也就是第五趟

主串abababacababaa
子串a

发生失配时,i=10,j=1;因为next[1]=0,所以我们让i++,j=1,继续匹配。
也就是第六趟

主串abababacababaa
子串a

主串abababacababaa
子串ababaa

匹配成功!

————————————————
本文在编写时部分借鉴了CSDN博主「Sirm23333」的原创文章,下面是原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值