kmp字符串匹配

1. what is kmp?

k m p kmp kmp是一种字符串匹配算法,用于在 s s s串中找到所有连续子串 t t t

由K什么,M什么,P什么三个人提出,所以叫 k m p kmp kmp

2.暴力算法

2.1 暴力中的暴力

如果是暴力的做法,我们会怎么做?
就直接拿 s s s的每一位当开头,然后往后走,就像这样

比如说我们现在我们要在 a b c a b a a b a b a a abcabaababaa abcabaababaa串中找 a b a b abab abab

那么暴力的做法显然就是这个样子的

在这里插入图片描述

但是这样肯定会很慢,复杂度 O ( ∣ s ∣ ⋅ ∣ t ∣ ) O(|s|\cdot |t|) O(st)
但是他慢在哪里了呢?

比如说在第四步的时候,我们发现a和c不匹配,但是我们接下来还是一步一步的跳到了第七步,但是因为第一个字符是a,很明显他不可能和b或者c匹配,所以我们可以直接跳到下一个开头的地方,而不用经过b,或者c

2.2 暴力中的优化

所以我们顺着上面的想法,我们想到先在 s s s子串中找到 t t t的第一个字符所在的位置,每次直接跳到下一个

这样的话…至少上面那个能快一半
但是如果遇到一个
s = a a a a a a a a a a ⋅ ⋅ ⋅ a a a a a a a a b c , t = a b c s=aaaaaaaaaa\cdot\cdot\cdot aaaaaaaabc,t=abc s=aaaaaaaaaaaaaaaaaabc,t=abc
就炸了

那么我们可以顺着这个思路去想

3.kmp算法

3.1 数组定义

next数组
k m p kmp kmp算法中,我们要设立一个数组 n x t [ i ] nxt[i] nxt[i]表示 t t t i i i个字符组成的字符串中相同前缀后缀的长度(C++编译器中 n e x t next next是关键字)

我们举个栗子

现在有一个子串是aababaaba,那么a和aaba都是他的相同前缀后缀,最长的就是aaba就是4

为什么要这样存呢?比如我们发现我们匹配到了最后一个a发现他是没有匹配上的,我们不需要把 t t t子串跳到开头,我们可以直接从第4位的a开始匹配,这样就节省了不少的时间

s,t字符串
在本文中,所有的字符串都是从1开始储存的,这样不会出现我们跳到负数的情况,减少了特判

其中我们要在 s s s串中查找 t t t

l s = ∣ s ∣ , l t = ∣ t ∣ ls=|s|,lt=|t| ls=s,lt=t

3.2 关于kmp难以理解的原因简要分析

k m p kmp kmp算是一种不太好理解的算法,原因我觉得主要有这样几种

  1. 各个文章的字符串储存方式都不大相同,有从0开始的,也有从1开始的
  2. 各个文章的的代码写法也有不少区别
  3. k m p kmp kmp的思想其实很简单,但是表达出来并不容易(呼之欲出的感觉),加上代码的边界特判问题也不易讲清

3.3 next数组的预处理

那么我们说了, n e x t next next数组是用来表示 t t t串中最长相同前缀后缀的长度的,所以我们需要在 t t t串中去自己匹配自己

先放下代码:

	for(int i=2,j=0;i<=lt;i++){
		while(j&&t[j+1]!=t[i])j=nxt[j];
		if(t[j+1]==t[i])j++;
		nxt[i]=j;
	}

这种写法是我认为比较好的一种写法,因为他不需要在同一个循环里来回跳 i , j i,j i,j我们每次循环里就把 i i i匹配好了就好了

整个算法的流程大概是这个样子的

  1. 显然,next[1]=0
  2. 对于处理next[i] (i>1)的情况时,如果我们发现当前的j+1(因为j最开始表示的是上一位的next值)和i不匹配,那么我们就一直往前跳直到跳到头或者匹配成功
  3. 如果匹配成功,那么next[i]=j+1(因为这两位相等,相同前缀后缀长度就是j+1),反之为0

那么这样我们就完成了对next数组的预处理
(当然如果你这里没有看懂可以往下看,有动图助于大家理解)

3.4 next数组的应用

我们现在已经知道了next数组,那么我们接下来应该怎么求解呢?

先上代码:

	for(int i=1,j=0;i<=ls;i++){
		while(j&&t[j+1]!=s[i])j=nxt[j];
		if(t[j+1]==s[i])j++;
		if(j==lt){printf("%d\n",i-lt+1),j=nxt[j];}	
	}

其实我们发现和上一个几乎是一模一样的,具体思路也差不多

给组动图吧
在这里插入图片描述
这是第一次我们在第一个位置匹配

此时我们发现 s s s t t t在第六位不匹配,我们看next[6]=2,我们就可以把 t t t串的第二位和 s s s的第六位进行匹配
在这里插入图片描述
然后 c o n t i n u e continue continue
在这里插入图片描述
我们看到 s s s串的第14位和 t t t串的第11位不匹配,我们看next[11]=5
我们就拿 t t t串的第五位和 s s s串的第14位继续匹配

后面也差不多就是这样了,大家可以自己手推一下

来个完整版的:
在这里插入图片描述

4. kmp时间复杂度证明

可以证明, k m p kmp kmp的时间复杂度是 O ( ∣ s ∣ + ∣ t ∣ ) O(|s|+|t|) O(s+t),或者说 O ( ∣ s ∣ ) O(|s|) O(s),因为 ∣ t ∣ ≤ ∣ s ∣ |t|\leq |s| ts

下面简单的说一下

我们发现 k m p kmp kmp整个函数执行过程中,一共只有两个操作

  1. j++,即匹配成功, O ( 1 ) O(1) O(1)
  2. j=next[j],匹配失败, O ( ? ) O(?) O(?)

但是我们发现,虽然2不可求,但是经过这样的匹配之后,j的值一定是在减少的,但是j不能减成负的,所以操作2最多做的次数和操作1一样多,那么这时候复杂度是 2 ∗ l e n 2*len 2len

因为在 k m p kmp kmp过程中,需要 t t t t t t匹配一次, s s s t t t匹配一次,所以在最坏情况下,复杂度等于 O ( 2 ∣ s ∣ + 2 ∣ t ∣ ) = O ( ∣ s ∣ + ∣ t ∣ ) = O ( ∣ s ∣ ) O(2|s|+2|t|)=O(|s|+|t|)=O(|s|) O(2s+2t)=O(s+t)=O(s)

5.模板题代码

# include <cstdio>
# include <algorithm>
# include <cstring>
# include <cmath>
# include <climits>
# include <iostream>
# include <string>
# include <queue>
# include <stack>
# include <vector>
# include <set>
# include <map>
# include <cstdlib>
# include <ctime>
using namespace std;

# define Rep(i,a,b) for(int i=a;i<=b;i++)
# define _Rep(i,a,b) for(int i=a;i>=b;i--)
# define RepG(i,u) for(int i=head[u];~i;i=e[i].next)

typedef long long ll;
const int N=1e6+5;
const int inf=0x7fffffff;
const double eps=1e-7;
template <typename T> void read(T &x){
	x=0;int f=1;
	char c=getchar();
	for(;!isdigit(c);c=getchar())if(c=='-')f=-1;
	for(;isdigit(c);c=getchar())x=(x<<1)+(x<<3)+c-'0';
	x*=f;
}

char s[N],t[N];
int ls,lt;
int nxt[N];

void kmp(){
	for(int i=2,j=0;i<=lt;i++){//t,t匹配
		while(j&&t[j+1]!=t[i])j=nxt[j];
		if(t[j+1]==t[i])j++;
		nxt[i]=j;
	}
	for(int i=1,j=0;i<=ls;i++){//s,t匹配(当然你也可以把这两个匹配写成一个函数调用两次)
		while(j&&t[j+1]!=s[i])j=nxt[j];
		if(t[j+1]==s[i])j++;
		if(j==lt){printf("%d\n",i-lt+1),j=nxt[j];}	
	}
}

int main()
{
	scanf("%s%s",s+1,t+1);
	ls=strlen(s+1),lt=strlen(t+1);
	kmp();
	Rep(i,1,lt)printf("%d ",nxt[i]);
	puts("");
	return 0;
}

6.写在最后

这篇文章参考自
https://blog.csdn.net/f1033774377/article/details/82556438(动图来源)
https://blog.csdn.net/hqw11/article/details/97504974(时间复杂度分析)

感谢 a k i o i akioi akioigjm2005,他让这篇文章没有鸽

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值