kmp算法和next数组求解


欢迎在评论区指正!

kmp概要

  通过观察暴力子字符串查找算法(又称简单的模式匹配算法、BF算法)不难发现,造成其时间开销大的原因是由于 i 指针的多次回溯导致产生了重复的匹配操作。因此,kmp算法充分利用了已匹配过的子串内容,通过将 j 设为某个值来使得 i 指针不回退,成功减少了不必要的重复操作。

kmp详细过程

  记录文本串s1和匹配字符串s2分别如下:

string s1 = "aabaabaaf";
string s2 = "aabaaf";

  若使用暴力匹配,在执行如下代码时:

//使用i标记s1,j标记s2
while(i < s1.length && j < s2.length) {
	if(s1[i] == s2[j]) {
		//如果相同则同时后移i j指针
		i++;
		j++;
	} else {
		//不相同回溯i j指针
		i = i - j + 2;
		j = 1;
	}
}

  发现,由于从s1[0]~s1[4]两字符串相同,直当匹配到s1[5]时失配,此时 i 指针需要回溯到 i=1,j 指针回溯到 j=1的位置(图解上表现为匹配串右移动)并重新开始匹配,使得前五次的匹配为无效匹配。那有什么办法能利用起前五次的匹配操作呢?kmp算法。kmp算法通过将模式串直接移动至下图位置,即在i=5不变的前提下,将模式串直接右移三位,大大减少了匹配的次数。

表1
01234567891011
aabaabaabaaf
aabaaf

  那么问题来了,kmp算法是通过什么来确定的右移位数呢?答案是部分模式匹配表(PMT)。

什么是PMT

  PMT中的值叫做部分匹配值(PM),意为失配位置前的子串最大的相等前后缀长度。
前后缀是求解PMT中的值的关键,相关概念较为简单,在此不做赘述。

string s2 = "aabaaf";

  s2的前后缀表如下。注意前缀不包含字符串本身,即不含最后一位,后缀同理,最长后缀不含第一位。

表2
字符串s2的前缀表
a
aa
aab
aaba
aabaa
表3
字符串s2后缀表
f
af
aaf
baaf
abaaf

  所以s2="aabaaf"的PM值为0。

PMT值的求解(手算过程)

  当失配位置为 j 时,字符串substring(s2, 0, j - 1)的最长相等前后缀长度即为PMT中 j 对应的PM值。

表4
失配位置失配前子串PM
0/失配位置前为空串,因此PM暂时还没法确定
1a0
2aa1
3aab0
4aaba1
5aabaa2

  那么这个PMT对kmp算法的帮助体现在哪里?为什么可以通过PMT可以知道模式串需要右移的位数不多不少刚好3位?
我们以字符串s1和模式串s2为例,第一次匹配时情况如下(绿色匹配,红色失配):

表5
01234567891011
aabaabaabaaf
aabaaf

  不难发现失配位置 i=j=5,通过PMT表发现失配位置为5时PM=2。这个PM=2代表了其失配前子串“aabaa”的最长相等前后缀长度为2,即{s2[0],s2[1]} == {s2[3],s2[4]},而在到达失配位置之前,算法已经依次比较过并得到结论{s1[3],s1[4]} == {s2[3],s2[4]},所以我们可以得到一个结果:{s2[0],s2[1]} == {s1[3],s1[4]} ,所以我们就可以将s2[0]移动到s1[3]的位置,即图5所示。

  至此,我们得到了右移位数为什么是3的原因。那么具体到每一步操作中,右移位数和PM的值应该是什么关系呢?观察表1和表5不难发现,右移位数move和PM的关系如下(需要注意到此时 j = i = 失配位置 = 5)
m o v e = j − p m move = j -pm move=jpm
  在图解中,我们移动了s2字符串的位置,但是在实际内存中s2的位置是固定不变的,因此只能通过移动 j 指针来达到相通的效果。向右移动字符串move位等于向左移动 j 指针move位。
j = j − m o v e = j − j + p m = p m j=j-move=j-j+pm=pm j=jmove=jj+pm=pm
  至此我们知道了在遇到失配时,i指针和j指针分别应该进行的操作是什么了:i 指针保持不变,j 指针移动到PMT中PM对应的数组下标位置。

此时我们回过头来看失配位置为的0时候的PM值。不难发现,无论是什么串,当字符串和匹配串的首位就发生失配时,很显然,只能让匹配串右移一位,而此时 j=0,因此失配位置0对应的pm值为:
p m = j − m o v e = − 1 pm=j-move=-1 pm=jmove=1

PMT机算过程(next数组的求解)

  写在前面:这里我的措辞写的很罗嗦,好像也说的不是很清楚,如果看不懂请移步这个视频:KMP算法之求next数组代码讲解 14分10秒开始。

  在聊机算过程之前,我想先聊聊我对next数组的一些感受。

  这一部分是整个kmp算法实现中最难理解的部分。b站上大部分的资料在这一块粗略讲过,同时,由于next数组的版本有许多种,很容易导致大家无法理解next数组。看过王道书的同学不难发现,在本篇文章中,我的next数组相较于王道书中的next数组都少了1。即同样的匹配串,我的next[]={-1,0,1,0,1,2}对应的王道书中的next[]={0,1,2,1,2,3}。可是pmt表不同,为什么通过公式得到的 j 需要回退到的位置都是等于next[j]呢?明明相差了1呀?这一开始让我百思不得其解。后来发现,王道书中对字符串s1、匹配串s2、pmt的处理都与我有一些差异:王道将字符串存储从数组的第二位开始,即字符串第一位存储于s1[1],默认弃用s1[0],同理,他也弃用了s2[0],next[0],至此,所有问题迎刃而解,王道中的字符串下标全部右移了一位,对应的 j 回溯位置也应该相应右移,所以他的pmt比我的pmt的值大1(这也是为什么22版p108在应用了我的表之后得到的最终公式中 j=next[j]+1)。而我个人习惯从数组的第一位即s1[0]起开始存储,因此,我的pmt会较王道少1。

  ok,我们回归到next的求解中来。观察以下匹配串:

index012345678
s2[]aabaacaba
next[]-101012

  假设目前已知道next[0]~next[5]的值,求next[6]。

  记 i 指针指向所求串的后缀最后一位,j 指针指向所求串的前缀最后一位。则图中这种情况下,j = 2,i = 5(这里不理解为什么指向这两个位置也没关系,往下看看代码就行)。

  此时我们已经知道next[5]的值为2,就说明{s2[0],s2[1]} == {s2[3],s2[4]}。此时,最好的情况也就是当s2[2] == s2[5]的时候,其最大相等前后缀长度为next[5]+1=3。

  但这里的s2显然不满足这种条件,此时就要对j进行操作。怎么操作?看下图:
在这里插入图片描述
  由next[5] == 2,next[2] == 1可以得到的信息是,两红框内容相等(有效信息为s2[0] == s2[3]),两蓝框内容相等,两绿框内容相等(有效信息为s2[3] == s2[4]),就可以得到a[0] == a[4]!此时就又回到了刚开始的问题:我们把 j 移动到下标为next[j](即j=next[5]=1)的位置,a[1]a[5]如果a[1]==a[5],那么next[6]=next[2]+1=2,但这里依旧不满足。那就再次把j移动到next[j](即j=next[1]=0),比较s2[j]和s2[i],相等+1,不相等继续这个操作。本例中,j最后=-1,表示此次匹配下来发现的最长相同前后缀长度为0,于是,将i后移一位用作记录新一个next的下标,同时为用作下一次匹配的后缀末尾指针;将j=0赋给next数组,移动j指针用作下一次匹配的前缀末尾指针(别忘了这里j已经=-1了,再后移一位指向的位置也就是s2[0])。

  为什么是j=next[j]?别忘了一开始我们把j指针定义为要比较的前缀的最后一个字符位置,而前缀的最后一个字符位置刚好就是next[j]的值(最长相等前后缀长度=>满足条件的前缀最长长度,对应的数组下笔标是该前缀的下一位)。

对应的代码如下:

void getNext2(string str, int next[]) {
    next[0] = -1; //一开始就初始化next[0] 肯定是等于-1的
    int i = 0, j = -1;
    while(i < str.length()) {
        if(j == -1){
            i++;
            j++;
            next[i] = 0;
        } else if(str[i] == str[j]) {
            i++;
            j++;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
}

  你是不是有所疑问,怎么这么麻烦?我看书上第一个if和第二个if是拼起来的呀?确实,当j=-1时,移动j的操作使得j从-1变了0,因此,第一个if的内容可以改成和第二个if的内容完全一样的代码,同时可以合并两个if,变成如下代码:

void getNext(string str, int next[]) {
    next[0] = -1;
    int i = 0, j = -1;
    while(i < str.length()) {
        if(j==-1||str[i]==str[j]){ //这里j==-1一定要写在比较前面 因为||在前一半成立的时候是不会判断后一半的,否则就要str[-1]报数组越界的错了
            i++;
            j++;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
}

从头开始过一遍kmp

01234567891011
i
aabaabaabaaf
aabaaf
j

01234567891011
i
aabaabaabaaf
aabaaf
j

此时失配,失配位置j=5,查next表将j移动Next[5]=2的位置

01234567891011
i
aabaabaabaaf
aabaaf
j

01234567891011
i
aabaabaabaaf
aabaaf
j

从i和j开始重新匹配 到i=8,j=5时失配
查next表得next[5]=2

01234567891011
i
aabaabaabaaf
aabaaf
j

从i和j开始重新匹配,到i=11 j=5时匹配成功

01234567891011
i
aabaabaabaaf
aabaaf
j
int index_KMP(string s1, string s2, int next[]) {
  int i = 0;
  int j = 0;
  while(i<s1.length() && j<s1.length()){
      if (j == -1 || s1[i] == s2[j]){
          //j==-1时的情况就是s1[i]开始是串连s2的第一个元素都不匹配时的情况,操作和s1[i] == s2[j]时一样,直接后移i和j指针。
          //或者可以理解为s2[-1] == s1[i]恒成立
          i++;
          j++;
      } else {
          j = next[j]; // 失配时移动j到next[j]的位置
      }
  }
  if (j >= s2.length()) { //王道书这里是大于没有等于,其原因也是他字符串开始位置位数数组第二位即s1[1]
      return i - s2.length();
  } else {
      return -1;
  }
}
int main() {
	//测试代码:
    string str1= "AABAABAAF";
    string str = "AABAAF";
    int next[str.length()];
    getNext(str, next);
    int i = index_KMP(str1,str,next);
    cout<<i;
    return 0;
}
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值