数据结构——KMP算法(难懂版,但还是看看吧)

据说这个算法很难,起初看了《大话数据结构》,知道了这个算法,但是没看懂没理解,然后看其他博客,尽管博客上写着易懂,好理解,但我仍然看不懂,不理解,心里一直在口吐芬芳。
后来我看了几个版本的KMP算法讲解,终于有所明目,所以来给大家写一个易懂版 难懂版的,直接硬刚。
因为是硬刚,所以字数难免长,我也没有动图,因为一开始就看动图我必看不懂动图,我相信有的人也是这样。但是我会尽量以唠嗑的形式跟大家讲讲我对这个算法的理解,让大家尽量不产生“太长我不看,讲的无聊”这种想法。当然你实在是一口气看不完,存起来看嘛。
好了,进入正题!
等等,先说明几个名词,我会统一+有所区别的说。别混了
主串(被匹配的串)
子串(含在主串或模式串里,不是整个模式串。别混
模式串(找主串去匹配的串)

目录

  • KMP算法的初步认识
    • 如何在主串和模式串某两对应字符匹配失败后确定j“指针”回到的指定的位置?(求next数组的关键问题——打通你任督二脉)
    • 如果您实在不愿看长篇大论或者您是大佬,就从这里看吧。

KMP算法的初步认识

  • 这是一个非常nb的算法,为什么会有这个算法,因为BF算法(朴素模式匹配算法,也称简单匹配算法等)时间复杂度高达O(m*n)(m,n分别为主、模式串的长度)。
  • KMP算法可以给他干到O(m+n),是不是效率有所提升?
  • 先给你丢一张图,你看看一个初步的过程,有所了解,再往下看。
    数据结构——KMP算法(难懂版,但还是看看吧)_第1张图片
    如果你觉得的看着很难受,那还是听我讲述一下KMP算法的目的和概览吧!
  • 首先我们从BF算法里了解到了,i这个整型变量“指向”主串的某个字符,j这个整型变量“指向”模式串的某个字符,而且i和j“指向”的两个字符一旦不匹配,i和j就要回溯。
  • 那么我们用KMP算法的本质目的,就是减少i和j这个“指针”的回溯,从而减少比较次数,让主串和模式串的两个指针“快速滑动”,赶紧让模式串找到主串中和模式串匹配的子串。
  • 好,怎么达到这个目的呢?我们先选择让i这个“指向”主串的指针——好马不吃回头草——要么原地不动,要么往前走
  • 什么时候i原地不动?那就是发生了模式串和主串的某个字符不匹配的情况,此时我们的j“指针”就要回到一个指定的位置,具体回到哪,我们先存疑,先往下看。如果你不往下看,你一直想,那你去死吧。
  • 什么时候i往前走?这还用说?当然是我模式串和主串的某个字符匹配上了啊!你主串的i“指针”不往前走,难道我模式串所有字符都跟你主串这一个字符比?放狗屁呢? 好了,我主串某字符和模式串某字符匹配上了,我自然的i、j主、模式串的“指针”就往后走一格去比较下两个对应的字符,然后再判断我i“指针”动不动、j“指针”回不回到指定的位置
  • 好,那我们“指向”主串的i“指针”和“指向”模式串的j“指针”的动作就安排好了,写代码呢,就是比较下i和j“分别指向的主串的某个字符和模式串的某个字符,比较过程我写在if条件语句里。
    走if我就认为主串和模式串的某两个字符匹配了,i、j往右边(往后头)走一格;else(主串和模式串的某两个字符不匹配),j回到某个指定位置。
  • 好!那么我们让j回到哪里去呢,这里就有讲究了。待我慢慢叙述拆解给你听。注意,我们研究j”指针“的回溯问题,就不要考虑主串了,你会发现你把主串的方方面面考虑进去,会很麻烦的。
  • 就单单举一个模式串的例子,如图所示
    在这里插入图片描述
    我们存储模式串是放进数组里存着的,一般是以下标1开头存模式串第一个字符,以此类推。
  • 然后我说明几个注意的地方:
    1.我们不可能只拿模式串和一个主串去比较,去匹配,我们写程序,必然会拿模式串和N个主串比,要不然你这个程序就是个死程序,不灵活。所以我们还举一个主串的例子就不合适了,也就是说我们不要把主串的方方面面都考虑进去。
    2.我们假设模式串的每一个字符都可能和主串不匹配,这很关键,这是求j”指针“回到某个指定位置的先决条件。
    3.接下来我会提到前后缀字符串,什么是前缀字符串和后缀字符串,我给你一张图,你就明白了,我拿文字说,你反而明白不了。
    数据结构——KMP算法(难懂版,但还是看看吧)_第2张图片
    如图所示,假如我j”指针“指向了模式串的最后那个A(实际上A后头可能还存在字符)。我发现右数第一个框的字符串和左数第一个框的字符串是相等的,为了方便叙述j回到某个指定位置,我就管右数第一个框的字符串称为后缀字符串,左数第一个框的字符串称为前缀字符串
    也有可能前后缀字符串不相等,我会放到如何求j“指针”回到哪个位置上来说怎么处理。你要是不懂,就接着往下看,你就可能会明白啦。

如何在主串和模式串某两对应字符匹配失败后确定j“指针”回到的指定的位置?(求next数组的关键问题——打通你任督二脉)

  • 首先大家可以先思考一下这个问题,就是假如我模式串某个字符和主串的某个字符比较了,不匹配,那么我模式串的这个字符前一直到模式串的头,这一模式串的子串和主串对应的子串肯定是匹配的吧!那要不然直接就在前面就发现有字符(这里的字符指主串和模式串对应的两个字符)不匹配了。什么?不信?你觉得,我第二或N次发现有字符不匹配了,哦,那我这一字符前面的所有字符都匹配?放狗屁吗?

  • 好,我发现了模式串某个字符(j指针指向的字符)和主串的某个字符(i指针指向的某个字符)比较,不匹配对吧。那么这个字符在模式串里是j指针指着(不打双引号了,大家心里明白就行),j指针前面的模式串子串是不是和主串对应的子串相等啊?看图!
    数据结构——KMP算法(难懂版,但还是看看吧)_第3张图片
    好的,然后我们对模式串的子串再观察,发现,是不是j指针左边的模式串的子串两端的子子串,如图所示
    在这里插入图片描述
    是不是相等的?不相等我一会儿会告诉你怎么办。 这里,子串两端的子子串,左端就叫做前缀字符串,右端就叫做后缀字符串。好,前缀字符串等于了后缀字符串,又因为我j指针前面的子串是匹配主串其对应的子串的,那么我是不是可以让前缀字符串移动到后缀字符串原先的位置上?如图(这里先说移动,往后会给一个符合计算机程序语言的说法,别在这死抠,没用。)
    数据结构——KMP算法(难懂版,但还是看看吧)_第4张图片
    好的,这只是个例子,我们再进行匹配,i和j指针边前移边比较指向的字符,哎,又到了一处发现不匹配了,那我们再看一下j指针指的字符前面的字符串,然后再找出相等的前后缀字符串,相等的前后缀字符串越长越好,越长,我们的j指针就越不用回溯到模式串的前几个字符那里。 找到了相等的前后缀字符串,就让前缀字符串跑到后缀字符串原先的位置上。

  • 经过可能的几轮匹配失败回溯,你会发现,不论主串的i指针指向的字符(或者说主串的字符)是什么如果模式串的j指针指向的字符和主串的i指针指向的字符匹配失败了,那么我只要找到了j指针前面的子串中有前缀字符串等于后缀字符串,那么我就可以移动这个模式串了,即我把前缀字符串移动到后缀字符串原先的位置上来移动整个模式串,这时,j指针看似没动,实际上已经是回溯到某个位置上了。为什么?因为你动了模式串了!

  • 好,找不到相等的前后缀字符串怎么办?就比如我模式串第一个字符和第二个字符发现和主串i指针指向的字符不匹配了。怎么办?那我就按照常规方法来呗,没法投机取巧那咱就正正当当的做事。就让j指针回溯到模式串的最前头那个字符的位置就OK了

  • 你会发现,首先,我判断i指针指向的字符和j指针指向的字符(主串和模式串字符)是否匹配,不匹配,我找j指针指向的字符前面那个子串,从那个子串里找相等的前后缀字符串,找到了,j回溯到一个指定的位置(前缀字符串移动到后缀字符串原先的位置上),找不到,j回溯到模式串第一个字符的位置上。

  • 是不是我除了判断主、模式串字符匹配的情况外,其他操作都是在模式串上进行的?所以主串的字符爱是什么是什么。只要主、模式串的字符不匹配了,那么我就在模式串那做事就可以了。因此,我可以把模式串摘出来剖析j具体回溯到哪个位置上。

  • 我们从头开始分析,假设模式串第一个、第二个、第三个字符就和主串的i指针指的字符不匹配了(我们从下标1开始存模式串的字符)。。。

  • 第一个字符失配,那我们就动i指针,往下指一个字符,再和模式串的第一个字符比较(j指针不用动)。

  • 第二个字符失配,那i指针不动,j指针回到模式串的第一个字符位置上,为什么,因为前后缀字符串长度不能大于j指针前的那个子串长,否则动模式串(回溯j指针)没有意义。所以也就没有前后缀字符串。好的,j指针回溯完再去和i指针指的字符比较。

  • 第三个字符失配,就要判定j指针前面的子字符串是不是有相等前后缀字符串了。有,那你纸上画一下移动前缀字符串到后缀字符串原先的位置上后,j指向了模式串的第几个字符。指向了模式串的第几个字符,就记录下来那的下标位置(第几个的几),然后拿目前j指向了的字符再和主串i指向的字符去比较看匹不匹配,然后再决定j回溯还是i、j同前移。这很关键!多看几遍。 如果没有,怎么办呀?那就让j指针回溯到模式串第一个字符的位置上啊!

  • 数据结构——KMP算法(难懂版,但还是看看吧)_第5张图片

  • 第N个字符失配,找j指针前面的前后缀字符串,看字符串长为1的前后缀字符串等不等、为2的前后缀字符串等不等,等的话长度是几,取最长的前后缀字符串, 然后去纸上或脑子里移动前缀字符串到后缀字符串原先的位置上,然后看j指向了模式串的第几个字符,记录下那个下标位置(第几个的几),然后再判断j和i指的两个字符匹不匹配。没有前后缀字符串相等这种情况,就让j指针直接滚回模式串第一个字符那里。(结合着上图来看)

  • 好的,把模式串所有的字符失配情况都分析完毕,如图。

    我把第一个字符失配的情况标记成0,然后其他位置的字符失配情况标记成j应该回溯的位置的数字,如图
    数据结构——KMP算法(难懂版,但还是看看吧)_第6张图片

然后我得到的这一串数字,就是除了第一个字符失配的时候的标记的数字外,其他数字,就是j应该回溯到模式串的第几个字符的位置。把这些数字包括0都囊进一个数组里,这个数组,就叫next数组,如图数据结构——KMP算法(难懂版,但还是看看吧)_第7张图片

如果您实在不愿看长篇大论或者您是大佬,就从这里看吧。

  • 好了,我们终于知道了next数组的值的求法了,无非就是模式串的所有字符挨个分析,都假设一遍失配。然后找相等且最长的前后缀字符串,找到了的话就在脑子里或纸上把前缀字符串移到后缀字符串原先的位置。移动完看j指针指向了第几个字符,这个几就是next数组对应的下标(即一开始发现模式串第几个字符失配了,下标就为几)的值。
    好了,说了这么多,你明白了吧?
    上代码!
//求next数组,结合着我上面分析的next数组求法来看
//你会明白的。
void get_next(SString T,int &next[])
{
	i=1;
	next[1]=0;
	j=0;
	while(i<T.length)//当i指针小于模式串的长度
	{
		if(j==0||T.ch[i]==T.ch[j])
		{//T.ch[i]是后缀字符串的单个字符
		//T.ch[j]是前缀字符串的单个字符)
		//请结合上面那个第一次出现红箭头的图来看,尤其是看图的后面
			++i;//这里的i就充当着我上面的next数组的每个值的分析里的j
			++j;//以备下次比较前后缀字符串的新增字符
			next[i]=j;//注意比较了前后缀字符串的新增字符,
			//再让i和j往后走,这样就符合我上面的分析,即
			//假如j指针指的字符失配了那么j指针往回回溯到哪里
			//即next数组应该是多少。
			//这更像是相等的前后缀字符串
			//的长度累积的过程,因为可能不断有前缀
			//字符串的字符等于后缀字符串的字符,
			//这样我相等的前后缀
			//字符串越长,那j回溯到模式串的某个字符的位置
			//就越远离模式串的开头(第一个字符)
			//这样我j指针指向的失配的字符下标即next[失配的字符下标]
			//的值是不是就越大啦?
		}
		else
		{
			j=next[j];//前后缀字符串的单个字符不相同
			//就让j回溯到指定的地方。其实这更像一个
			//从最长的前后缀字符串是否相等开始判断直到
			//最短的前后缀字符串是否相等或者根本不存在
			//相等的前后缀字符串,万一有了最长的相等的
			//前后缀字符串,那我就不用找短的相等的了,
			//省时间吧?找不到呢?那就找有没有短的相等的
			//前后缀字符串呗(通过if条件语句里
			//前后缀字符串的单个字符比较),直到短的相等的前后缀
			//字符串也没有了为止。
			//没有了j直接等于0
		}
	}
}
int index_KMP(SString S,SSTring T,int pos)
{
	int i=pos,j=1;
	int next[255];
	get_next(T,next);//求模式串对应的next数组的所有值
	while(i<S.length&&j<T.length)
	{
		if(j==0||S.ch[i]==T.ch[j])
		{
			i++;
			j++;
		}//和BF算法一样。匹配了i、j指针往后走就是了
		else
		{
			j=next[j];//i不变,j回溯
		}
	}
	if(j>T.length)
	{
		return i-T.length;//匹配成功,返回主串中匹配的子串的第一个字符位置
		//这么写是因为模式串最后一个字符和主串i指针指的字符匹配后i还会++。
		//所以就这么些就完了不需要-1。
	}
	else
	{
		return 0;//匹配不成功,也就是主串没有子串和模式串匹配的上。
		//返回匹配失败的标志
	}
}

然后还有一个next数组(KMP)算法改进是吧,这个呢,我给你贴个代码先

//next数组算法改进(修正)
void get_nextval(SString T,int &nextval[])
{
	i=1;
	nextval[1]=0;
	j=0;
	while(i<T.length)//当i指针小于模式串的长度
	{
		if(j==0||T.ch[i]==T.ch[j])
		{//T.ch[i]是后缀字符串的单个字符
		//T.ch[j]是前缀字符串的单个字符)
		//请结合上面那个第一次出现红箭头的图来看,尤其是看图的后面
			++i;//这里的i就充当着我上面的next数组的每个值的分析里的j
			++j;//以备下次比较前后缀字符串的新增字符
			if(T.ch[i]!=T.ch[j])//如果后缀字符与前缀字符不同
			{
				nextval[i]=j;//和next数组算法一样。
			}
			else
			{
				nextval[i]=nextval[j];//如果后缀字符等于前缀字符,
				//就将前缀字符的nextval值赋值给nextval在i位置的值
			}
		}
		else
		{
			j=nextval[j];//前后缀字符串的单个字符不相同
			//就让j回溯到指定的地方。其实这更像一个
			//从最长的前后缀字符串是否相等开始判断直到
			//最短的前后缀字符串是否相等或者根本不存在
			//相等的前后缀字符串,万一有了最长的相等的
			//前后缀字符串,那我就不用找短的相等的了,
			//省时间吧?找不到呢?那就找有没有短的相等的
			//前后缀字符串呗(通过if条件语句里
			//前后缀字符串的单个字符比较),直到短的相等的前后缀
			//字符串也没有了为止。
			//没有了j直接等于0
		}
	}
}

嗯,你再结合这张图看代码,你就知道next数组(KMP)算法是怎么改进的了。
数据结构——KMP算法(难懂版,但还是看看吧)_第8张图片
数据结构——KMP算法(难懂版,但还是看看吧)_第9张图片
实际上你会找找规律算nextval值就可以了,代码什么的不要求掌握。图和代码结合着看就可以明白啦。

啊哈,说了这么多,你,应该,可以,理解了吧?实在不理解我也没辙了,那我讲的可能有些失败。。。但是我觉得我注释标了这么多,解析写了这么多,你一遍读不懂,读两遍,慢慢来别着急。可以看懂的,看不懂的话也可以评论或私信问问我,或许你不懂的地方我也没懂但我没注意到!

参考:
《大话数据结构》——程杰著
《数据结构与算法基础》视频课——up主:青岛大学——王卓
《KMP算法(易懂版)》视频课——up主:天勤率辉

你可能感兴趣的:(数据结构,算法,数据结构,算法,字符串)