KMP 字符串匹配算法

每日吐槽,KMP算法绝对是我见过最难的算法之一了,不知道是不是自己脑子不够用,还是咋滴,又或是最近太放飞自我了——学习1小时,玩手机一上午,我竟然看了整整两天才把这个算法看懂,嗷呜,不说了不说了,默默流下不争气的眼泪!

书看了好几遍,视频也听了好几个,然而我还是无法领会这神秘的KMP算法,没办法丫,我只好在那疯狂的看博客,看了一篇又一篇,最后,终于找到了一篇我能看懂的,没错,就是下面这篇博文,让我彻彻底底理解了KMP算法:
链接https://www.cnblogs.com/yjiyjige/p/3263858.html
感谢博主,好人呐!简直就是我的救命恩人。

1.算法引入:

难归难,但不得不说,KMP算法真的是非常天才的一段编程,让我看看这是who想出来的,哦原来是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的。而KMP刚好就是三位大神的名字组合,我直呼好家伙!确认过眼神,都是我仰慕的人儿。

学过算法的人应该都知道,KMP是一个知名度很高的算法,几乎所有的《数据结构》书中都会提到它,那么现在问题来了,KMP算法是干什么用的呢?为啥它这么出名呢?
kmp算法又叫“看毛片”算法,是一个效率非常高的字符串匹配算法,它要解决的问题是求解模式串(也叫子串,称它为P)在主串(称它为T)中的位置。给定两个字符串,判断其中一个字符串是否包含另一个字符串,如果包含,则返回被包含串在包含串中的起始位置。

对于这个问题,有一种很常规的求解思路,那就是暴力枚举,从主串的第一个字符开始,从左到右依次进行匹配,在这个过程中如果存在某个字符不匹配,则将模式串向右移动一位,重新进行匹配。
算法过程如下:

首先初始化计数指针i和j,用i和j分别指示主串T和模式串P中当前正在进行比较的字符

依次比较i和j指向的字符是否一致,若一致,则i和j同时向右移动,继续比较下一个字符

若i和j指向的字符不一致,如上图中A和E不一致,则模式串P向右移动一位,指针i回到下标为1的地方,指针j回到下标为0的地方,继续进行比较

根据以上分析,我们可以写出下面的程序:

int match(string T, string P) {
	int i = 0, j = 0;
	while (i < T.length() && j < P.length()) {
		if (T[i] == P[j]) {
			i++;  
			j++;
		}
		else {
			i = i - j + 1;
			j = 0;
		}
	}
	if (j == P.length())   return i - j;
	else return -1;
}

暴力枚举算法能够解决字符串的匹配问题,但它绝不是一个好的算法,指针的频繁回退造成了大量不必要的字符比较操作,以致于它的时间复杂度达到了O(mn),而KMP算法在暴力枚举算法的基础上,进行了优化和改进,将时间复杂度降到了O(m+n)

2.KMP算法

对于暴力匹配算法,在第一趟匹配过程中,i=3处的字符与j=3处的字符不一致,于是又从i=1,j=0处重新开始比较,但是仔细观察,我们会发现,i=1,j=0和i=2,j=0这两趟比较是无需进行的,为什么呢?因为假如让我们人为寻找的话,我们就不会这样做,匹配失败的字符A之前的三个字符都是匹配的,除了第一个字符,就没有字符A了,那么即使将指针i移动到下标1和下标2处,分别与模式串中的第一个字符进行匹配,匹配也必将以失败告终。

这时我们脑海中会产生这样一种想法:保持指针i不动,只移动指针j至下标为0处,进行字符的比较操作,这样就可以避免指针i的无用回溯,从而降低算法的时间复杂度,而这便是KMP算法的思想:“利用已经部分匹配这个有效信息,保持指针i不回溯,通过修改指针j,让模式串尽可能移动到有效的位置”

那么,当遇到模式串中的字符与主串中的字符不匹配的情况时,我们应该将指针j移动到哪里呢?

接下来,让我们来思考:

字符C和D不匹配了,我们应该将j移动到哪呢?那当然是下标1处丫,因为前面有一个字符A相同

同样下图也是一样的道理,很容易发现,应把j移动到下标2处,因为在此之前,有两个字符相同

仔细观察上面两个例子的P中失配字符之前的字符串,我们会发现,它有着相同的前后缀,假如一个字符发生失配后,j要移动到k,那么满足以下性质:下标k处之前的k个字符,和下标j处之前的k个字符是相同的,用公式表达如下:
P[0~k-1] = P[j-k~j-1]

那么即是当满足条件 P[0~k-1] = P[j-k~j-1] 时,可将指针j移动到下标k处,证明如下:

当 **T[i] != P[j]**时

T[i-j ~ i-1] == P[0 ~ j-1]

P[0 ~ k-1] == P[j-k ~ j-1]

必然有:**P[0 ~ k-1] == T[i-k ~ i-1] **

在模式串P的每一个位置处都有可能发生失配的情况,而每一个位置j对应一个k,这样就会产生多个k了,通常我们用一个next数组来存储这些k,next[j]=k,表示j的下一个位置是k,那么重点来了,怎么求出这些k值呢?我认为next数组的求解是整个KMP算法最关键的地方,是重点,也是难点,我当时就是卡在这个地方了,所以一直搞不懂kmp算法到底在讲个啥?

既然写不出来代码,那咱们就先来看代码好了

void get_next(string P, int * next) {
	int j = 0, k = -1;   //next[j]=k
	next[0] = -1;
	while (j < P.length() - 1) {
		if (k == -1 || P[j] == P[k]) {
			next[j + 1] = k + 1;
			j++;
			k++;    //这三行等同于 next[++j] = ++k;
		}
		else {
			k = next[k];  //寻找较短的相同前后缀
		}
	}
}

对于next[j]的求解,我们先来考虑边界情况:

j=0失配时,怎么办?此时j已经在最左边了,不能再向左边移动了,这时将i向右移动,初始化 next[0]=-1

j=1失配时,又该怎么办呢?很显然,j只能移动到下标0处,此时初始化 next[j]=0

那么一般的情况,已知 next[j]=k,我们应该怎么求next[j+1]呢?

next[j]=k,那说明 P[0~k-1] = P[j-k~j-1],此时让我们求 next[j+1],我们会很容易想到分两种情况讨论,哦豁,这怕是在做数学题,哈哈哈,我忍不住优雅的笑了!

当 P[k] = P[j]时,有P[0~k] = P[j-k~j],那么此时 next[j+1] = k +1,也即是 next[j+1] = next[j] +1,如果不信的话请看下面的图,正所谓有图有真相嘛

当 P[k] != P[j] 时,比如下图所示:

我们已经不可能找到 ABAB 这个最长的相同前后缀了,那么此时我们的目标就应该转向寻找较小的相同前后缀,因为我们还是可以找得到像 AB这样的相同前后缀,这样我们就能明白为什么 P[k] != P[j] 时,k = next[k]了,图示中,k = next[k] = 1,P[k]之前的A和P[j]之前的字符A相同,这很容易证明,此时若有P[k] == P[j],那么必有 next[j+1] = k + 1,简直完美,perfect!

至此,最难的next数组求解完毕,nice ! 接下来就是kmp匹配过程的代码了,小事情!不过要时刻记得一句话:next[j]=k,表示当模式串中第j位的字符与主串不匹配时,就把指针 j移动到下一个位置k下标处。好了,开始码代码啦!

//kmp算法求index
int kmp_index(string T, string P, int next[]) {
	int i = 0, j = 0;
	while (i < T.length() && j < P.length()) {
		if (j == 0 || T[i] == P[j]) {
			i++; j++;
		}
		else {
			j = next[j];
		}
	}
	if (j == P.length())  return i - j;
	else  return -1;
}

Game over,生活不易,喵喵叹气!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值