手撕KMP,带你秒掉KMP(不懂next数组的请进)

引入

KMP算法是专门用来解决字符串匹配问题的,什么叫字符串匹配问题呢?就是说给定两个字符串 s,t。我们称s为文本串,t为模式串,字符串匹配问题就是查找s中的一个子串,使得这个子串和模式串完全相等,我们要求的就是是否存在这么一个子串,如果存在返回这个子串的位置或存在多少个这样的子串(诸如此类的问题)。

KMP

说明:
s : 文本串
t : 模式串
n : s的长度
m : t的长度

我们先来考虑一下暴力算法,我们肯定会枚举s中长度为m的所有子串,检查它是否与模式串相等。这样做的时间复杂度是 O ( n 2 ) O(n^2) O(n2),而KMP算法实际上是暴力算法的一个优化,他的思想是当某一个位置失配时,直接跳过一段距离,以来优化其。我们先来看一个例子:

123456789101112131415161718
s:abacbacbababacabae
t:ababaca

第一次匹配,s的第1位与t的第1位相对应当我们匹配到第4个字符时发现失配了,在暴力算法中,我们会将模式串后移一位重新匹配,但在KMP中,我们直接将模式串后移到第3个位置,从第4个位置重新开始匹配,因为我们发现模式串的第一位与第三位相等,而我们没有必要去将模式串头移动到第2位(反正已经失配了),而移动到第3位也是一个优化,因为第一位与第三位相等,而1~3位都已经匹配上了,我们这样移动刚好可以不用重新匹配第一个字符(t[1]=t[3],t[1…3]=s[1…3],所以后移之后t[1]已经等于s[3],不需要重新匹配),直接从第二个开始匹配。

后移之后的文本串与模式串对应关系如下:

123456789101112131415161718
s:abacbacbababacabae
t:ababaca

也就是说模式串的前3个字符组成的串中,长度为1的前缀与长度为1的后缀相等(t[1]=t[3]),这样,对于模式串的每个位置,都存在这样一个前缀与后缀,使得前缀与后缀相等,并且这一对前后缀是满足上述性质的所有前后缀中最长的一对。这个最长的前后缀就叫做border。

我们将每个位置的border的长度就存在next数组中,如果说 n e x t [ i ] = j next[i]=j next[i]=j,那么就表示以第 i 个字符为结尾的长度为 j 的子串与从第 1 个字符开始的长度为 j 的子串,也就是对于模式串中前 i 个字符所组成的子串,我们要求一个最大的 j,使得在这个子串中长度为 j 的前缀等于长度为 j 的后缀,这个 j 就是next[i]。

这里需要注意的一点是这里所说的前缀和后缀一定是原字符串的严格前后缀,也就是前后缀不等于原串。

对于上述例子中的模式串 a b a b a c a ababaca ababaca,它的next数组值如下:

ababaca
0012301

那么next数组该怎么求呢,这里就要用到一点动态规划的思想,如下:

假设next[1]=j.
j=next[i-1].

我们从i=1开始求,next[1]是肯定等于0的(他连子串都没有)。

根据动态规划,我们在求next[i]之前已经求出了next[i-1]…next[1]的所有值,我们要求next[i],可以抽象一下,我们是在把next[i-1]所对应的border后缀加入了一个新的字符t[i],相对应的,我们对应的前缀也要加入一个字符t[j+1],所以说我们只需要看t[i]是否等于t[j+1],如果等于,next[i]=j=j+1,因为j=next[i-1]已经是我们能找到的最长的对于t[1…i-1]的border长度,那么它们加1一定是next[i]的值。

那如果不相等呢?注意,重点来了,我们首先要明白一个点,因为字符串中两个前后border完全相等,那么前border的前 b o r d e r 2 border_2 border2一定等于后border的后 b o r d e r 2 border_2 border2,例如对于上述例子ababaca,他有一个子串ababa,这个子串的border是’aba‘与‘aba’,而‘aba’的border是‘a’,应为’aba‘==’aba‘(border的定义),所以说‘aba’(前缀)的前border——‘a’,就等于‘aba’(后缀)的后border——’a‘。

看到这里,你是不是若有所思,那么既然有了上述这个点,我们就可以将t[1…i-1]这样递归下去,递归式就是 j = n e x t [ j ] j=next[j] j=next[j].也就是得到比next[i-1]更短的前后缀中最长的border。然后再次判断t[i]是否等于t[j+1] 。如果等于,那么 n e x t [ i ] = j + 1 next[i]=j+1 next[i]=j+1,如果不等于,那么我们就这么一直递归下去,直到它等于或是 j = = 0 j==0 j==0的时候(j==0时无法向下递归)。

那么,梳理一下,得到求next数组的流程如下:

  1. 假设 j = n e x t [ i − 1 ] j=next[i-1] j=next[i1],如果说 t [ j + 1 ] = t [ i ] t[j+1]=t[i] t[j+1]=t[i],那么 n e x t [ i ] = j + 1 next[i]=j+1 next[i]=j+1(见上文)。并直接返回结束。
  2. 若不相等,那么 j = n e x t [ j ] j=next[j] j=next[j]以来寻找对于子串 t [ 1... i − 1 ] t[1...i-1] t[1...i1]中长度小于 n e x t [ i − 1 ] next[i-1] next[i1]的前后缀中最长的border。然后,如果 t [ j + 1 ] = t [ i ] t[j+1]=t[i] t[j+1]=t[i],那么 n e x t [ i ] = j + 1 next[i]=j+1 next[i]=j+1 。并直接返回结束。
  3. 如果此时仍不相等,则重复第2步,直到其相等或j=0为止。

这种DP算法下,求next数组的时间复杂度是 O ( m ) O(m) O(m),而进行字符串匹配的时间复杂度为 O ( n ) O(n) O(n),所以总体的时间复杂度为 O ( n + m ) O(n+m) O(n+m),其渐近复杂度为 O ( n ) O(n) O(n).

Code:

#include<bits/stdc++.h>
using namespace std;
string s,t;//s 文本串,t 模式串
int nxt[1000005];//next数组
int m;
void getborder(){
	nxt[1]=0;
	//k等价于上文的j
	for(int i=2,k=0;i<=m;i++){
		while(k&&t[k+1]!=t[i]){//递归下去直到t[j+1]==t[i]或j==0
			k=nxt[k];
		}
		if(t[k+1]==t[i]){//判断新加入的两个字符是否相等
			k++;
		}
		nxt[i]=k;
	}
}
void KMP(){
	//k表示的是当前模式串的前k个已经被匹配
	for(int i=1,k=0;i<=s.size()-1;i++){
		while(k&&t[k+1]!=s[i]){//失配了,就跳到border后缀的位置,这里把模式串后移转换成了把当前的模式串下标置为next[k].
			k=nxt[k];
		}
		if(t[k+1]==s[i]){//匹配上了,k++继续往下走
			k++;
		}
		if(k==m){//已经被匹配的数量达到了m,说明模式串完全被匹配
			cout<<i-m+1<<'\n';//输出模式串出现的位置
			k=nxt[k];//继续向下一步走。 
		}
	}
}
int main(){
	cin>>s>>t;
	s=" "+s;
	t=" "+t;
	//使其下标从1开始
	m=t.size()-1;
	getborder();
	KMP();
	return 0;
}

我把我KMP跳枪课程都给大家了,大家给个赞再走呗

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值