数据结构-KMP算法(附详推next数组及代码讲解)

复习考研数据结构时遇到了这个KMP算法,听说它被誉为数据结构第三大难学的算法,学完之后感觉雀实有点小绕,因此小陈想写下来,以备将来不时之需。

要想理解这个算法,我们先从一个简单的例子说起:

比如,给你两个字符串,一个作为主串,一个作为子串,(主串比子串要长),让你从主串中找到和子串相同的部分,并求出主串是从哪一位置开始和子串相同的。

大家肯定都知道怎么求,各个位循环匹配不就行了?(这样的方法其实叫做简单的模式匹配算法)

雀实,这么暴力的干,我们终究会得到结果,但是如果遇到比较大,比较复杂的主串和子串时,它的效率并不高,举个栗子吧

主串设为:A B A B C A B C A C B A B

子串设为:A B C A C

当你利用这种简单的模式匹配算法时,它的匹配过程是这样的:
在这里插入图片描述

(子串循环匹配到第3位就发现和主串不一样了,将整体移到下一位开始新一轮的循环匹配)
在这里插入图片描述

(子串循环匹配到第1位就发现和主串不一样了,将整体移到下一位开始新一轮的循环匹配)

在这里插入图片描述

(子串循环匹配到第5位就发现和主串不一样了,将整体移到下一位开始新一轮的循环匹配)

在这里插入图片描述

(子串循环匹配到第1位就发现和主串不一样了,将整体移到下一位开始新一轮的循环匹配)

.

.

.

一直以这样的步骤重复的对比,直到最后相等

⬇⬇⬇⬇⬇⬇

图片

这样做其实是比较麻烦的,它的时间复杂度是O(mn),m和n分别是主串和子串的长度。

他的麻烦在于,你会发现,

在第1和第2步之间,完全可以让子串从主串的第3位开始

在第3到第4步之间,完全可以让子串从主串的第6位开始

如果可以避免上面例子中不必要的重复的比较,那我们应该怎么做呢?

由此,我们正式引入KMP算法。

KMP算法:

了解KMP算法之前,我们先来引入3个概念,有利于我们后续理解。

①前缀②后缀③部分匹配值

举个栗子帮助大家理解一下这三个概念吧,我们以“ababa”为例吧

1:“a”,前缀无,后缀无,最长相等前后缀长度:0.

2:“ab”,前缀{a},后缀{b},最长相等前后缀长度:0.

3:“aba”,前缀{a,ab},后缀{a,ba},前后缀同时都有“a”,所以最长相等前后缀长度:1.

4:“abab”,前缀{a,ab,aba},后缀{b,ab,bab},前后缀同时都有“ab”,所以最长相等前后缀长度:2.

5:“ababa”,前缀{a,ab,aba,abab},后缀{a,ba,aba,baba},前后缀同时都有“aba”,所以最长相等前后缀长度:3.

所以字符串“ababa”的部分匹配值(PM)为:00123

同理:所以字符串“abcac”的部分匹配值为:00010.

我们现在可以建立一个关于字符串和部分匹配值的表:

在这里插入图片描述

在这里插入图片描述

第①趟发现第3(j)位不匹配时,再移动就有了规律:

移动位数=已匹配的字符数(j-1) - 对应的部分匹配值(PM[j-1])

即第1趟检测出不匹配后,子串整体直接右移2两位,让子串的第1位与主串的第3位对应,并开始比较。

(若子串第1位即不匹配,则可在代码中实现将子串循环整体右移1位,或者也可以假设PM[0]=-1再应用以上公式)

↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

这就是KMP算法的思想所在

只是理解了这个算法的思想,那我们离应用和做题还差了一大截,接下来,我们接着讲KMP改进方法代码,以及KMP如何再优化

由上面的规律可知,当第j位不匹配时,要移动的步数(Move)等于已匹配的字符数(j-1) - 对应的部分匹配值(PM[j-1])位,即:

Move=(j-1)-PM[j-1]

这样每次不匹配时,都要回去找到第(j-1)位才可以进行计算,有些麻烦,所以我们可以将PM表整体右移一位,即:
在这里插入图片描述

第1位置-1,防止第1位就不匹配,最后一位溢出,也不用再管,所以,上面的公式就变为了:

Move=(j-1)-Next[j]

所以相当于将子串的“比较指针”j回退到:

j=j-Move

(用通俗一点的话来说就是:"老指针"所指向的不匹配元素,减去应该回退的步数,即为"现指针"应指的元素)

(这里的"指针"是通过数组和其下标来实现的一种标记,并不是真正的指针)

将Move的值带入,即等价于

j=j-Move=j-((j-1)-Next[j])=Next[j]+1

所以,我们又可以为了方便,将Next表再整体加1。(目的是为了把j=Next[j]+1 后面的+1消掉)

所以新的子串和Next数组的对应关系表即为:

图片

这样:

j=Next[j]

即:在子串的第j个字符与主串发生失配时,则跳到子串的Next[j]位置重新与主串当前位置进行比较

这时大家会发现,如果第1位时就不匹配,j将等于0,这显然不对劲。所以,我们需要在代码里实现对这种情况的控制:

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

当子串的第1位与主串不匹配时,while中if语句判定是失败的,只能进入else语句中,j将置0,但是i此时不变,第二轮开始时,由于j=0,直接进入while中的if语句中,此时,i从1自加为2,j从0自加为1,相当于从子串的第1位与主串的第2位比较。

即:做到了子串在第1位不匹配时,"整个子串后移"一位的操作。

当子串的第1位与主串匹配时,while中的if语句判定成功,一切按部就班,一位接一位的往下比较,i也始终大于j,没有任何问题。

到这里大家基本上已经对KMP算法的工作原理了如指掌了,但是其实说完上面的还不够,算法中还有一个影响效率的因素存在

那就是,next数组需要人为的去计算,计算完后还要手动输入到程序中,太麻烦了…

有没有什么方法让程序自己去求一下next数组呢?有,来往下看:

先设一个子串为:“S1S2S3…Sn”

如果该子串在和主串对比时,第j个字符失配,我们应该怎么滑动子串去和主串继续匹配?

因为在第j个字符失配了,所以前面j-1个字符一定是和主串相同的字符。

再设一个k值,用来代表:重新匹配时,子串开始与主串对比的位置。

由我们上面得到的规律可知:

S1S2…Sk-1=Sj-k+1Sj-k+2…Sj-1

举个栗子:

在这里插入图片描述

(只是演示一下k值的应用,实际k值是多少需要让程序自己计算)

针对这个方法,有以下2种特殊情况:

1、子串已匹配的相等字符序列中,不满足条件:S1S2…Sk-1=Sj-k+1Sj-k+2…Sj-1,(即可看作k=1时)就要右移j-1位,让主串第i个字符和子串第1个字符进行比较。

在这里插入图片描述
⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
在这里插入图片描述

2、子串第1个字符与主串第i个字符失配时,规定next[1]=0。

(可理解为将主串的第i个字符和子串的第1个字符前面空位置对齐,也即子串整体右移一位)

所以,我们得到了一个关于next的函数公式:

在这里插入图片描述

我们现在知道:next[1]=0, 再设next[j]=k。

所以next[j+1]=多少呢?

只有两种情况:(因为前后缀如果相等的话,那么从最后一位往前一定是相等的,所以我们要从最后一位比起,即:只判断Sk和Sj )

一、若Sk=Sj 则表明子串中:

在这里插入图片描述

即next[j+1]=next[j]+1

二、若Sk≠Sj 则表明子串中:
在这里插入图片描述

这时,可以把求next函数值视为又一个模式匹配问题,(即套娃求值嘿嘿)

通俗来讲就是,把Sj-k+1Sj-k+2…Sj当作主串,把 S1S2…Sk当作子串,让他们两个找到更短的相同前后缀。(即把不匹配的两个子串进行缩短,再匹配是否有相等的前后缀)

如下图:

在这里插入图片描述

这样类推下去,直到找到一个k’

此时,

k’=next[next…[k]] (1<k’<k<j)

所以,

next[j+1]=k’+1

如果这个k’不存在,则

next[j+1]=1

举个栗:
在这里插入图片描述

一:求next[7]

因为next[6]=3 所以,需比较 S6和S3 , 因为S6 ≠ S3 ,又 next[3]=1

所以比较S6 和S1 ,又S6 ≠ S1

所以不存在相等的前后缀,所以next[7]=1

二:求next[8]

因为next[7]=1 所以,需比较 S7和S1 , 因为S7 = S1

所以next[8]=next[7]+1=2

二:求next[9]

因为next[8]=2 所以,需比较 S8和S2 , 因为S8 = S2

所以next[9]=next[8]+1=3

求next数组的代码:

非常简单,自己把代码在纸上走一遍就会了

void get_next(String T ,next[]){
 int i=1,j=0;
 next[1]=0;
 while(i<T.length){
   if(j==0||T.ch[i]==T.ch[j]){
   ++i;++j;
   next[i]=j;
   }
   else
     j=next[j];
 }
}

KMP算法到此基本可以说完结了,不过他还具有更妙的改进方法,可以更好的避免重复比较操作。

KMP算法的改进:

举个缺陷的例子吧先:

在这里插入图片描述

很明显,在第4位失配时,模式串和主串按照next数组去匹配时,是一位接着一位的去匹配的,而不是直接将模式串的第1位右移到主串的第5位的,导致了重复操作。

所以,面对这种情况,我们要对next数组进行加工才可以避免,即再次递归,将next[j]修正为next[next[j]],循环下去,一直到前后两个next数组不再相等为止。将修正后的数组命名为nextval。

求nextval的代码:

非常简单,自己把代码在纸上走一遍就会了

void get_nextval(String T ,nextval[]){
 int i=1,j=0;
 nextval[1]=0;
 while(i<T.length){
  if(j==0||T.ch[i]==T.ch[j]){
   ++i;++j;
   if(T.ch[i]!=T.ch[j]) nextval[i]=j;
   else  nextval[i]= nextval[j];
   }
  else
     j=nextval[j];
 }
}

恭喜你看完了整个文章,希望这篇文章让你对程序底层世界的了解更加清晰。

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值