算法学习笔记:KMP 算法

本文深入探讨了KMP算法在字符串匹配问题中的应用,通过自匹配操作求解Next数组,减少不必要的匹配次数,从而提高效率。文章详细介绍了KMP算法的原理、字符串匹配过程及时间复杂度分析,并提供了相应的代码实现。KMP算法通过利用前后缀关系避免了暴力匹配的重复计算,降低了时间复杂度。
摘要由CSDN通过智能技术生成

1.概述

KMP 算法是一种字符串算法,具体解决的问题为字符串匹配问题:

给出一个模式串 t t t,文本串 s s s,请问 t t t 是否为 s s s 的字串 / t t t s s s 中出现了几次等等问题。

后文中无特殊说明, n n n 为文本串 s s s 的长度, m m m 为模式串 t t t 的长度。

2.例题

link

我们要求两个东西:

  1. 模式串 t t t 在文本串 s s s 中出现的位置。
  2. 模式串 t t t b o r d e r border border 长度。

我们暂且先不管这个 b o r d e r border border,考虑第 1 个问题。

显然有一种暴力匹配的方法:直接从最前面的位置开始,在文本串 s s s 中截出长度为 m m m 的字串, O ( m ) O(m) O(m) 暴力匹配。

然而,这样做会发现最坏理论复杂度到了 O ( n m ) O(nm) O(nm)!我们不能忍受。

于是 KMP 就出现了。

比如面对这样的两个字符串:

文本串 s = a a a b a c a b a c d
模式串 t =   a b a

规定起始点为 i i i

如上所示,假设我们现在匹配到了 i = 2 i = 2 i=2 的位置。我们发现不匹配(失配),那么如果是暴力做法,那么在 i = 3 i = 3 i=3 的位置就会重新匹配一次。

但是我们完全可以不用这么做,因为 模式串 t t t 的前面两个字符跟文本串 s s s 的前面两个字符相同,可以直接从第 3 位开始匹配。

而 KMP 的任务就是尽可能多的实现上面这句话。

那么我们又怎么判断要跳到哪里呢?

还记得 b o r d e r border border 吗? b o r d e r border border 可以帮助我们判断要跳到哪里。

比如说我们现在在文本串第 i i i 个位置开始匹配,结果失配了,假设失配位置为 j j j,那么我们直接从 b o r d e r j border_j borderj 开始匹配。

下文记 N e x t i Next_i Nexti b o r d e r i border_i borderi

想想为什么? b o r d e r border border 的定义是:在一个字符串 s s s 中,如果一个串 s t r str str 满足其既是 s s s 的前缀,又是 s s s 的后缀,那么 s t r str str 就是 s s s 的一个 b o r d e r border border,而一个串的 b o r d e r border border 指他的所有 b o r d e r border border 的最长长度。

那么如果模式串 t t t 失配,我们可以跳到 N e x t t Next_t Nextt 这个位置继续匹配,而不需要直接从头匹配。

那么如何求 N e x t Next Next 数组呢?

2.1 自匹配操作

N e x t Next Next 数组在 KMP 中称为自匹配操作。

比如对于模式串 t = a b a b a c a b a \text{t = a b a b a c a b a} t = a b a b a c a b a,我们要对其进行自匹配操作。

注意: N e x t i = i Next_i=i Nexti=i 是没有意义的!

首先显然的, N e x t 1 = 0 Next_1=0 Next1=0

那么 N e x t 2 Next_2 Next2 呢?还是等于 0。

N e x t 3 Next_3 Next3?等于 1。

但是我们是怎么知道 N e x t 3 Next_3 Next3 等于 1 的呢?上图!

在这里插入图片描述

比如我们要求 N e x t i Next_i Nexti,而此时我们已经保证 N e x t 1... i − 1 Next_{1...i-1} Next1...i1 已经求好。

上图中红色部分表示这个串的 b o r d e r border border

N e x t i − 1 = j Next_{i-1}=j Nexti1=j,那么我们假设 j j j 在这个位置:

在这里插入图片描述

由于上图中两个蓝色部分完全相同,那么我们首先判断一下 t j + 1 t_{j +1} tj+1 t i t_i ti 是否相同,如果相同 N e x t i Next_i Nexti 就求出来了。

但是不相同呢?或许有的人会说了:那不是还要暴力查找吗?

不需要!因为 N e x t i − 1 = j Next_{i-1}=j Nexti1=j,此时如果我们再取 k = N e x t j k=Next_j k=Nextj(为了方便擦去了红色部分):

在这里插入图片描述

图很丑(确信

那么首先在 [ 1 , j ] [1,j] [1,j] 内两段绿色字符串相等,而由于 N e x t i − 1 = j Next_{i-1}=j Nexti1=j,根据传递性, [ 1 , k ] [1,k] [1,k] 就会跟 [ i − k , i − 1 ] [i-k,i-1] [ik,i1] (也就是最后这段绿色的)相同,此时我们只需要判断 t k + 1 t_{k+1} tk+1 是否等于 t i t_i ti 就可以了。相同就结束,不相同?继续这么做呗!

所以我们会发现,实质上 KMP 充分利用了 b o r d e r border border 的性质,以 N e x t Next Next 数组为媒介,减少了转移次数,从而降低时间复杂度。

不过需要注意:当 j j j 跳到 0 时,如果 t 1 ≠ t i t_1 \ne t_i t1=ti,此时 N e x t i = 0 Next_i = 0 Nexti=0;否则其余所有情况, N e x t i = j + 1 Next_i = j + 1 Nexti=j+1

那么在 t = a b a b a c a b a \text{t = a b a b a c a b a} t = a b a b a c a b a 中, N e x t 3 = 1 Next_3=1 Next3=1 也就不难想了吧!

对于这个文本串 t t t N e x t = { 0 , 0 , 1 , 2 , 3 , 0 , 1 , 2 , 3 } Next=\{0,0,1,2,3,0,1,2,3\} Next={0,0,1,2,3,0,1,2,3}

于是自匹配操作漂亮解决。

代码:

int j = 0;//初始化为 0
for (int i = 2; i <= m; ++i)
{
	while (j && s2[j + 1] != s2[i]) j = Next[j];//不断往前找
	if (s2[j + 1] == s2[i]) ++j;//注意 +1
	Next[i] = j;
}

其实此时你会发现,题目要求的 b o r d e r border border 长度就是我们的 N e x t Next Next 数组。

这里说句闲话:在几年之前 luogu 的模板题是说:

直接输出 N e x t Next Next 数组。
如果你不知道什么是 N e x t Next Next 数组,自行学习 KMP 算法。

不过或许是因为一些原因(比如并不是所有人的 KMP 都用的是 N e x t Next Next 的,也有人用 f a i l fail fail 命名),最后变成了输出 b o r d e r border border 长度。

2.2 字符串的匹配

那么回到我们的问题:求模式串 t t t 在文本串 s s s 内分别出现在哪几个位置。

现在有了 N e x t Next Next 数组,再加上我们前面说的,应该不难想了。

首先我们先初始化 j = 0 j=0 j=0,然后开始暴力匹配。

当我们发现 t j + 1 = s i t_{j+1}=s_i tj+1=si 时,匹配成功, j j j 右移。

否则, t t t s s s 失配,此时根据我们最开始所说的,我们将 j j j 重置为 N e x t j Next_j Nextj 继续匹配。

当完全匹配到一个字符串时,我们输出位置 并且重置 j = N e x t j j=Next_j j=Nextj(这点非常重要!否则在下一个位置匹配的时候 j j j 会被重置为一些奇奇怪怪的东西,导致操作失误,想知道的读者可以自己尝试)

那么这就是 KMP 的字符串匹配过程。

代码:

j = 0;
for (int i = 1; i <= n; ++i)
{
	while (j && s2[j + 1] != s1[i]) j = Next[j];//不相同就跳
	if (s2[j + 1] == s1[i]) ++j;//注意 +1
	if (j == m) {printf("%d\n", i - m + 1); j = Next[j];}//一定要重置!
}

2.3 时间复杂度分析

KMP 的时间复杂度有一点迷。

在随机数据下:

对于每一个 i i i 位置,我们在匹配字符串时(包括自匹配)正常情况下 j + + j++ j++ 只会执行一次,那么 i i i 从 1 到 n n n j j j 从 1 到 m m m,互不干扰,时间复杂度为 O ( n + m ) O(n+m) O(n+m)

但是很遗憾的是据说 KMP 比较容易被卡成 O ( n m ) O(nm) O(nm) 的时间复杂度,不过作者目前还没有找到 hack 数据。

还是 hash 好,稳定的 O(n) 算法

2.4 代码

话说上面都放出来了还有必要再放一遍吗

代码:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int MAXN = 1e6 + 10;
int n, m, Next[MAXN];
char s1[MAXN], s2[MAXN];

int read()
{
	int sum = 0, fh = 1; char ch = getchar();
	while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
	while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
	return sum * fh;
}

int main()
{
	scanf("%s", s1 + 1);
	scanf("%s", s2 + 1);
	n = strlen(s1 + 1); m = strlen(s2 + 1);
	int j = 0;//初始化为 0
	for (int i = 2; i <= m; ++i)
	{
		while (j && s2[j + 1] != s2[i]) j = Next[j];//不断往前找
		if (s2[j + 1] == s2[i]) ++j;//注意 +1
		Next[i] = j;
	}
	j = 0;
	for (int i = 1; i <= n; ++i)
	{
		while (j && s2[j + 1] != s1[i]) j = Next[j];//不相同就跳
		if (s2[j + 1] == s1[i]) ++j;//注意 +1
		if (j == m) {printf("%d\n", i - m + 1); j = Next[j];}//一定要重置!
	}
	for (int i = 1; i <= m; ++i) printf("%d ", Next[i]);
	printf("\n"); return 0;
}

3.总结

KMP 的思想其实就是充分利用各个前后缀之间的关系,使得我们在字符串失配的时候不至于从头开始匹配,从而大大降低时间复杂度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值