KMP算法详解

  1. 学好KMP算法,关键点有三点,一是知道什么是前缀和后缀最长相等长度;二是理解KMP算法流程,三是求解next数组。

一、什么是前缀和后缀最长相等长度

首先给定一个字符串abstkabs。下面介绍前缀和后缀

前缀就是以第一个字符开始,一共能找出多少种和本身不一样的字符串(必须连续)。意思就是你不能破坏原来字符串的顺序来找,比如abstkabs,abtk就不行,因为它破坏了原来的顺序结构,即从b直接跳到t。所以abstkabs的前缀有a、ab、abs、abst、abstk、abstka、abstkab;不计算本身,所以共7种。同理,后缀是以最后一个字符结尾,一共有多少种和本身不一样的字符串。此处后缀有s、bs、abs、kabs、tkabs、stkabs、bstkabs 共7种。而前缀和后缀中只有abs这个字符串是相等的,所以前缀和后缀中最长的长度就是当前缀和后缀都为abs的时候,即最长相等长度为3。理解这个概念很重要,这也是next数组的含义。

二、KMP算法的流程

1、简单概念介绍:

KMP算法简单的说就是匹配字符串S1,S2,是否S2为S1的子串,当然你要满足s2的长度是小于等于S1。比如absabs和bsa,明显bsa是包含于absabs中,并且连续,所以bsa为absabs的子串。而kmp算法的返回值是如果S2是S1子串,则返回第一个匹配相等的字符的下标,所以这个例子,就是返回1,如果不是子串直接返回-1。

2、从暴力解法引入KMP算法

注意!!!图中的数字或者字母代表字符串中某个字符的下标(或者理解成指针),不要和字符串的内容混淆

如果针对两个字符串匹配的问题,暴力解法必然是如下所示:拿一个比较通用的情况来举例

即当S2和S1一直开始匹配,直到某个匹配过程的时候,匹配到不相等的地方,也就是S1[X]和S2[Y](绿色表示字符串的内容匹配一致)。这时候暴力解法就是S2向右移动一位,也就是拿S2重新跟S1从i+1处开始进行匹配。然后依次类推。这样一来时间复杂度是o(N*M)级别的,因为假设S1有N个,S2有M个(M<=N),考虑最坏情况:

我需要从S1第一个字符开始看,如果和S2一致,则要往下一直要看M个字符,直到发现最后一次比较S1和S2对应的值不相等,那么我得重新开始从第二个字符开始继续看。对于第一个字符就是o(1*M),S1一共N个字符,所以需要看N*M遍,所以复杂度为o(N*M)。复杂度还是很高的。而KMP算法其实在暴力的基础上跳过了一些字符的比较,所以加速了比较进程。

KMP算法是怎么样流程呢?还是针对上面那幅图

 当S1和S2匹配到不相等的时候,即S1[X]!=S2[Y],KMP是根据S2中的前缀和后缀的最长相等长度(后面一致称为最长前缀,因为两者相等),将指针Y往前指向到Y处的最长前缀(不含Y,只看Y之前字符串的最长前缀)的下一个,再跟S1中X处开始比较。可能你听不太懂啥意思,看下面的图应该比较好理解:

 假设此时Y处之前的最长前缀和后缀是红框中所示,那么Y就指到到最长前缀的下一个,也就是:

 然后再和S1的X处进行匹配,如下图虚线对应的那样:(j是最长后缀的第一个字符,在下面讲解有用)

 其实就相当于把S1往左移动,也就相当于把S2往右移动,反正只要保证X和Y对应就行。但这里我习惯性的是理解成S2右移动(因为S1是你的参照物,最好不动,然后S2是你要匹配的,所以尽量让S2移动,我觉得这样比较好理解)。所以就相当于Y向右移动五格,所以X和Y对齐,0和j对齐:

至此,其实KMP算法的性质就变了,也就是变成问你,S1从j位置开始,可不可以实现S2的匹配?并且,我只需要从X和Y处进行比较即可。

你可能会问,为什么可以直接跳到j位置开始匹配呢?这是因为j的位置是最长后缀的第一个字符,而0的位置是最长前缀的第一个字符,将他俩对齐我可以保证j~x-1这段内容和0~y-1这段内容是一致的。因此我就可以直接从S1[X]和S[Y]处进行比较。但是问题又来了,你怎么保证j之前的任意位置开始,S1配不出S2?我们可以用反证法来证明:

假设i~j中间有一个位置k,且从k位置开始可以实现S2的匹配(图中红框的部分表示S2的最长前缀和后缀)即:

如果在假设成立的前提下,那么必然有什么?因为假设告诉你,从k开始可以实现s2的匹配,所以必然有k~x-1的长度的内容必然和S2中从0开始等长度的内容是一致的(如果这一段都不一致,后面怎么可能是匹配的嘛),也就是黄色框内的内容。即S1黄=S2黄。这样会出现什么问题?因为绿色的内容是表示匹配,所以会出现:

 S2紫色框和S1黄色框的内容相等(都是绿色),S1黄= S2紫。而S1黄=S2黄。所以有S2的黄色框=S2的紫色框,S2黄= S2紫。那这代表什么?不就代表S2的最长前缀和最长后缀变成了S2黄和S2紫了吗?而我们之前明明给定了最长前缀和最长后缀是红框了呀,所以出现就前后矛盾,所以假设不成立。这就说明i~j中不可能出现某个位置实现S1对S2的匹配,那么自然为什么可以直接从j位置开始匹配的原因。

上面就是KMP算法的核心思想。接下来讲一下流程:假设两个指针i1和i2分别指向S1和S2的第一个字符。分为种情况

1.当S1[i1] = S2[i2]的时候,两个指针同时往后走,i1++,i2++;

2.当S1[i1] != S2[i2]的时候,其实也就是我们上面讲述的一大堆所代表的某种中间情况,即在某一个位置,两者不匹配了。

  •     2.1 如果i2可以往前跳:即next[i2]!=-1或者i2!=0(因为next[0]=-1),则i2跳到最长前缀的下一 个,即i2=next[i2];再跟可能x处对比(其实就是找j位置)
  •     2.2.当i2不能往前跳的时候,i1++;这步是什么意思?其实就是i2一直往前跳,跳到头, 还没找 到s1中可以和我对比的位置,也就是j位置(上述已讲)。怎么说?因为第2步,它就是往前  跳,跳到一个位置可以去跟x处对比(相当于可以找到一个j位置,让s1从j位置开始,对s2进行匹配)。而此处i2跳到头还没找到一个位置可以和x位置对比,那么说明从s1的x处开始一直往前,根本没有一个j位置可以匹配s2,所以这时候,s1需要往下移动一个位置作为新的X处,然后再重复过程即可。

下面给出KMP的主代码。

int KMP(string&s1, string&s2) {
	if (s1.empty() || s2.empty() || s1.size() < s2.size()) return -1;
	vector<int> next = getNextArray(s2);//构建小串s2的next数组
	//开始匹配,首先定义s1和s2的指针都从0开始
	int i1 = 0;//指向s1的首元素
	int i2 = 0;//指向s2的首元素
	while (i1 < s1.size() && i2 < s2.size()) {
		if (s1[i1] == s2[i2]) {//当两者指向的值相等,两个继续往下指,看后续是否一致
			i1++;
			i2++;
		}
		else if (i2!=0) {//当两者指向的值不相等的时候,并且i2不是指向第0个位置(因为next[0]=-1)i2往前跳到最长前缀的下一个
		//else if(i2!=0)也可以这样写	
			i2 = next[i2];

		}
		else {//当两者指向的值不相等,并且指向第0个位置,那么i1要往后面移动一个位置跟我再比
			i1++;
		}
	}
	return i2 == s2.length() ? i1 - i2 : -1;
	/*
	i2如果越界,则表示匹配完成,所以,返回的第一个匹配的下标就是i1 - i2,否则返回 - 1
	注意,为什么是i1-i2?因为我们要知道,i2是不停跳跃变化的,但是唯一关心的就是当匹配完成的时候,
	它一定会到最大下标的下一个(越界),这样才能保证s2比完,所以i2==s2.size(),就是看它越界没,
	如果越界,则第一个匹配的下标,是啥,是不是i1当前的位置,减去i2的长度(也就是s2.size())?对吧
	*/	
}

三、next数组

next的数组的含义一定要搞清楚,首先next数组是针对较小的字符串而言的,换句话说就是需要匹配的字符串,比如abbs和bbs,那么next数组就是针对bbs而言。其次,该数组的每个位置的值(或者叫信息)其实是跟该位置没有关系,它其实表示的是它前一位置的信息,这个信息是什么呢?就是该位置的前一位置的最长前缀(最长后缀)的长度。

我们人为的规定,next[0]=-1,next[1]=0;所以next数组要从i=2开始计算。比如bbs这个字符串,它的next数组就是[-1,0,1],因为i=2,是s,它保存的信息是s之前(跟S无关)的字符串的最长前缀和最长后缀的长度,所以就是1。

了解完next数组的含义,我们来看看next数组怎么求解。假设我们现在要求字符串str 的i位置的值,那么根据之前说的,我们需要找的是i之前的字符串的最小前缀的长度。而next[i]必然跟next[i-1]相关。我们设i-1前的最长前后缀为红框所示,且最长前缀的下一位的位置用cn表示

其实也分为种情况:

1.str[i-1]==str[cn],那么很显然,i之前的最长前后缀长度就变成了3+1=4;即:next[i+1]=cn+1

2.当str[i-1]!=str[cn]:

  •  2.1:且cn并不处于第0个位置,也就是cn可以往前跳,那么cn就跳到next[cn]的位置,

        即cn=next[cn];next[cn]的值是什么?它表示cn处的信息,也就是cn之前的字符串的最长前后缀长度。假设上图红框中还有最长前后缀,即:

       这时继续比较cn和i-1处是不是相等,相等则next[i+1]=cn+1,即next[i]=1+1 = 2;

  •  2.2 cn处于0位置,说明cn不能往前跳了,因此i之前就没有最长前缀和后缀,所以next[i+1]=0;

next数组实现:

vector<int> getNextArray(string& str) {
	if (str.size() == 1) return { -1 };
	//定义一个next数组
	vector<int> next;
	next.resize(str.size());
	next[0] = -1;
	next[1] = 0;
	int i = 2;//从i=2开始计算
	int cn = 0;
	/*注意:为什么这里cn=0?
		cn表示拿什么位置跟i-1位置对比,也就是一直拿cn位置(前缀的下一个)跟i-1位置对比。
		因为i从2开始计算,而i-1也就是1,所以就是拿什么位置跟i=1对比?i=1之前就一个字符,也就是说没有前缀,所以cn=0
		此外,cn还表示i-1的信息,因为cn = next[i-1],比如next[1] = 0,这就是为什么初始值设置为i=2,cn=0
	*/
	while (i < next.size()) {
		if (str[i - 1] == str[cn]) {//如果str的i-1处字符和cn处相等,那么next[i]=next[i-1]+1,继续往下比
			next[i++] = ++cn;//cn也是当前next[i-1]的值,所以next[i]=cn+1。因为要继续看下一位是否同样满足,所以同时自增
		}
		else if (cn > 0) {//并不是0位置
			cn = next[cn];
		}
		else {
			next[i++] = 0;
		}
	}
	return next;
}

 最后写个main函数测试一下:

int main()
{
	string big;
	string small;
	cout << "请输入大小字符串" << endl;
	cin >> big;
	cin >> small;
	int ans = KMP(big, small);
	cout<< ans<<endl;
	system("pause");
	return 0;



}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值