KMP算法思路梳理

KMP算法

什么是KMP算法?在字符串中查找指定的子串并返回位置,找不到返回-1

注:代码中部分代码是伪代码

算法过渡:

在下面的算法中我们都以字符串ABABCD为例

一.朴素模式匹配算法(KMP算法的由来)

char c[7]={‘ ’ ,‘A’,’B’,’A’,’B’,’C’,’D’}
我们拿子串ABC去匹配,看看过程是怎样的:
第一轮匹配(指针从1往后移动):

c[1]c[2]c[3]c[4]c[5]c[6]
主串ABABCD
待匹配串ABC
c[1]c[2]c[3]c[4]c[5]c[6]
主串ABABCD
待匹配串ABC
c[1]c[2]c[3]c[4]c[5]c[6]
主串ABABCD
待匹配串ABC

由于c[3]匹配不上,进行第二轮:

c[1]c[2]c[3]c[4]c[5]c[6]
主串ABABCD
待匹配串ABC

由于c[2]匹配不上,进行第三轮:

c[1]c[2]c[3]c[4]c[5]c[6]
主串ABABCD
待匹配串ABC
c[1]c[2]c[3]c[4]c[5]c[6]
主串ABABCD
待匹配串ABC
c[1]c[2]c[3]c[4]c[5]c[6]
主串ABABCD
待匹配串ABC

匹配上了,算法结束!

综述:可以看出朴素模式匹配算法是从主串ABABCD的第一个位置开始往后匹配,如果发现后面匹配不上,就把主串的指针挪到下一个位置继续从子串的头部重新开始匹配。

引出:如果遇到主串aaaaaaaaaaa,拿着子串aaab去匹配,就会造成主串的指针频繁的回溯,效率低下,所以K、M 和 P 这三个人就研究出来一种算法,使得主串指针不用回溯也可以达到相同的目的,这就是著名的KMP算法!

二、KMP算法

我们说主串的指针不用回溯,究竟是怎样一回事呢?来看看:

先上代码:

// 用S代表主串,P代表待匹配的子串
int KMP(S, P)
{
  int i=1, j=1;
  while(i<=S.length && j<=P.length)
  {
    if(j==0 || S[i]==P[j])
    {
      i++;
      j++;
    }
    else
    {
    	j=next[j];  
    }
  }  
  if(j>P.length){
    return i-P.length;
  }
  return -1;
}

next数组是什么?匹配不上时子串指针该移动到指定的位置,我们跟着下面的流程走一遍就明白了:

为了效果更明显,我们设主串为AAABAAC,子串为AAC

第一轮:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC

当主串指针移动到c[3]时,发现子串和主串不匹配,主串指针不用回溯,将子串的指针移动到第二个位置,进行第二轮:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
子串指针0123

然后主串指针和子串指针继续向后匹配:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC

发现c[4]位置主串和子串不匹配,将子串指针移动到第二个位置:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
子串指针123

发现还是不匹配,继续将指针移动到第一个位置:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
子串指针123

发现不匹配,将子串指针移动到0位置:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
子串指针0123

然后,主串和子串指针同时向后匹配:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC

匹配成果,返回!看完之后是不是很懵?为什么知道子串该移动到哪?下面来看规则:

在第一轮中,c[3]位置匹配不上:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC

可以肯定的是c[3]前面的串一定匹配上了,现在就是让子串指针移动到某个位置使得亮色字符串中主串中的尾部子串中的头部匹配上就可以了,一图胜千言 :

在这里插入图片描述
找主串中和子串中颜色相同的尾部和头部

名词解释:

  • 主串中的尾部:不包含第一个字母的串,例如ABCD的尾部为 D、CD、BCD 总共三个。我们称{D、CD、BCD}为后缀
  • 子串中的头部:不包含最后一个字母的串,例如ABCD的头部为A、AB、ABC 总共三个。我们称{A、AB、ABC}为前缀

求出主串和子串的前后缀:

主串后缀:A

子串前缀:A

所以子串指针移动到第二个位置:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
子串指针123

指针继续向右匹配:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC

指针走到c[4]位置发现不匹配了,然后继续找主串的后缀和子串的头部相同的串:

主串后缀:A

子串前缀:A

所以子串的指针移动到第二个位置:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
子串指针123

此时,发现c[4]不匹配

主串后缀:∅

子串前缀:∅

所以子串的指针移动到第一个位置:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
子串指针0123

发现还是不匹配,子串的指针移动到0位置:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
子串指针0123

然后主串的指针和子串的指针同时移动进行匹配:

c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC

规则到此就演示完了,所以你会发现这就是找前缀和后缀,然后取最长相同的前后缀,我们把前缀和后缀匹配出来的信息存到一个next数组里面,子串在哪个位置匹配失败就去next数组的哪个位置取它的指针的位置。

所以,子串AAC的next数组为:

索引next[1]next[2]next[3]
012

如何手动计算字符串的next数组?

用字符串AAABAAC为例,演示下方法:

  • 第一个位置匹配不上:
c[0]c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串CCC
子串指针0123

第一个位置匹配不上,说明应该和主串指针后面的值进行比较,只有把子串的指针移动到0位置,然后主串和子串的指针同时后移:

主串指针c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串CCC
子串指针0123

所以next[1]=0

  • 第二个位置匹配不上:
c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串ACC
子串指针123

主串的前缀和子串的后缀都为空,所以next[2] = 1

  • 第三个位置匹配不上:
c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAC
子串指针123

能匹配上的串为AA

主串的后缀:A

子串的前缀:A

所以next[3]= 2

  • 第4个位置匹配不上:
c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAAZ
子串指针1234

能匹配上的串为AAA

主串的后缀:A,AA

子串的前缀:A,AA

所以next[4]=3

  • 第五个位置匹配不上:
c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAABZ
子串指针12345

能匹配上的串为AAAB

主串的后缀:B,AB,AAB

子串的前缀:A,AA,AAA

所以next[5]=1

  • 第六个位置匹配不上:
c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAABAZ
子串指针123456

能匹配上的串为AAABA

主串的后缀:A,BA,ABA,AABA

子串的前缀:A,AA,AAA,AAAB

所以next[6]=2

  • 第七个位置匹配不上:
c[1]c[2]c[3]c[4]c[5]c[6]c[7]
主串AAABAAC
子串AAABAAZ
子串指针1234567

能匹配上的串为AAABAA

主串的后缀:A,AA,BAA,ABAA,AABAA

子串的前缀:A,AA,AAA,AAAB,AAABA

所以next[7]=3

所以字符串AAABAAC的next数组为:

索引next[1]next[2]next[3]next[4]next[5]next[6]next[7]
0123123

逻辑清晰了,再看看代码是不是就明白了呢!
小试牛刀
我们将主串S="AAABAAC"和子串P="AAC"和子串的next数组带入程序中试试呗:

// 用S代表主串,P代码待匹配的子串
int KMP(S, P)
{
  int i=1, j=1;
  while(i<=S.length && j<=P.length)
  {
    // j==0就说明主串中当前指针所指元素和子串中第一个元素不同,next[0]=0,指针+1后指向第一个位置
    // S[i]==P[j]说明可以匹配,两者指针同时后移
    if(j==0 || S[i]==P[j])
    {
      i++;
      j++;
    }
    else
    {
      // 移动子串的指针到指定位置
    	j=next[j];  
    }
  }
  if(j>P.length){
	return i-P.length;
  }
  return -1;
}

三、如何用程序计算next数组

上面我们是手算的next数组,那怎么用程序来计算next数组呢?

上代码:

void getNext(char c[],int next[]){
	int i=1,j=0;
	next[1]=0; // 第一个匹配不上就移动到子串的第0个位置
	while(i<c.length-1){
		if(j==0||c[j]==c[i]){ // j等于0 或者 主串和子串的当前指针位置的值相等时,两者指针同时后移
			i++;
			j++;
			next[i]=j;
		}else{// 子串和主串匹配不上时,此时子串指针在哪个位置,就去next数组里面取指针该移动到的目标位置
			j=next[j];
		}
	}
}

四、next数组的优化

next数组是针对于匹配串而言的,也就是子串才有next数组

在上面我们手动算出字符串AAABAAC的next数组为:

next数组next[1]next[2]next[3]next[4]next[5]next[6]next[7]
0123123

在匹配过程中,匹配串指针移动到第二个位置时:

索引1234567
匹配串AAABAAC
next数组值0123123

发现不匹配,此时匹配串指针移动到next[2]也就是1位置,但是我们发现位置2为A匹配不上,位置1也为A肯定也匹配不上,所以指针要继续移动到next[1]也就是0位置,避免指针重复移动,我们直接把next[2]的值修改为0:

索引1234567
匹配串AAABAAC
next数组值0023123

匹配串指针移动到第三个位置时不匹配,指针就移动到next[3]也就是2位置,发现2位置与3位置匹配不上的字符相同,所以next[3]更新为next[2]的值:

索引1234567
匹配串AAABAAC
next数组值0003123

重复以上步骤,得到最终的next数组:

索引1234567
匹配串AAABAAC
next数组值0003003

我们将优化后的数组命名为:nextval 数组。

最后,有问题欢迎大家留言讨论,写得不对的地方希望大神多多指点。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值