KMP算法

我们要解决的问题:在指定字符串中,查找给定的子字符串的位置

KMP算法本质上就一句话:做过的事情就不要重复做。

KMP算法实现形象表述:首尾交集有多少

我们后面慢慢理解这几句话。

我们先举一个例子,一个关于翻牌的游戏。第二行每个字母代表一个人持有的牌,代表未知的牌。规则是对应的人在对应的位置上翻到对应的牌。比如持有牌a的人,他需要找到字母为A的牌;持有牌a,b的人,需要翻到牌A,B。

情形一:现在假定只有一个持有牌a人,他翻到牌A,他需要找到下一张牌A:

 A*******************
a                   

因为前面的牌都是未知,所以持有牌a的人只能一张一张的去翻看,每次翻看一张,移动一格(=1-0

情形二:假定两个人a和b对应位置上找到了牌A和B,现在要去找下一个位置的牌A和B:

12345678910
AB********
ab









ab





因为b已经翻看了第二张牌,知道是B了,a也就不需要再去翻看就知道不是自己要的牌了。所以a直接跳到第三张牌,b翻看第四张牌,移动两格(=2-0


情形三:对于持有牌a,b和c三个人,假定在对应位置上

1234567891011121314151617181920
ABC*******


*

*********
abc



















abc













很显然,牌B,C已经知道了,不适合a,b,所以跳过,移动三格(=3-0

1234567891011121314151617181920
AB
C
*******


*

*********
abc



















abc















情形四:对于持有牌a,b,c,a的人,假定翻到了对应的牌A,B,C,A,现在需要找下一个位置的牌A,B,C,A
1234567891011121314151617181920
ABC
A
******


*

*********
abca


















abca












查看发现第一个人喝第四个人都持有牌a,所以找下一个位置时,第一个人不需要去翻牌了,因为第四个人已经翻到了一张A,其他人继续翻牌就可以了,移动三个(=5-2


情形五:对于持有牌a,b,c,a,b的人,假定翻到了对应的牌A,B,C,A,B现在需要找下一个位置的牌A,B,C,A, B


1234567891011121314151617181920
AB
C
A
B
*****


*

*********
abcab

















abcab











查看发现第一个人,第二个人和第四,第五个人持有牌a,b,所以找下一个位置时,第一,第二个人不用去翻牌子,直接用第四,第五个人翻出的牌A,B就可以了,其他人继续翻牌子。

好了现在我们来理解我们开始总结的一句话:做过的事情不要重复做——在这里就是翻过的牌,不需要再去翻看,因为已经有人翻过了,适合不适合都已经知道了(适合的直接用,不适合直接跳过)。那我们如何知道适合不适合呢?这就要持牌人提前知会下——分两种情况:没有持有相同的牌人(情形一,二,三)直接跳过;有持有相同牌的人(情形四,五),对应上的直接用。


一句提前知会就可以了吗?如何知会呢?这就是next数组解决的问题。

还是上面的规则,我们换一个例子。假定持有牌:a,b,a,b,a,c,a的人,可以存在数组里arr[]={a,b,a,b,a,c,a}



前一个人翻牌成功:没有相同持牌人(因为只有一个人,没法比),人数k=0 ---> next[0] = 0

a 
 a

前两个人翻牌成功:相当于在a的基础上,b加入,没有相同的持牌人,人数k=0 ---> next[1] = 0

ab



ab

前三个人翻牌成功:相当于在a,b基础上,a加入,发现新加入的第三人持有的牌和第一个人持有相同的牌a(arr[2] == arr[0]) 有相同持牌人,k= k+1 = 0+1=1 ---> next[2] = 1

aba



aba

前四个人翻牌成功:在上面基础上,加入b,发现新加入的第四人持有的牌和第二个人持有相同的牌a(arr[3] == arr[1]),有相同的持牌人,k=k+1 = 1+1 =2 ---> next[3] = 2

abab



abab

前五个人翻牌成功:在上面基础上,加入b,发现新加入的第五人持有的牌和第三个人持有相同的牌a(arr[4] == arr[2]),有相同的持牌人,k=k+1 = 2+1 =3 ---> next[4] = 3

ababa



ababa

前六个人翻牌成功:在上面基础上,加入c, 这时c!= b,即arr[5] != arr[3],这时我们很为难。既然新加入的c不与b匹配,那我们要重新确认多少人可以利用前面人翻过的牌。人数增加可定是不可能了,只能减少,所以往两边拉开,是一格一格的拉开吗?

ababac



ababac
我们关注下背景是黄色的字符:
ababac



ababac

然后与三个人翻牌成功的情况进行对比,可以发现是一样的,那么持有相同牌的人数也是一样的(有连续一人),于是得到(跳过一格,移动了两格)

ababac







ababac

然后发现新加入的c != b,即arr[5] != arr[1],那我们只能继续拉开首尾。拉开多少?这又要看持有相同牌的黄色背景部分。这不就是只有一个人翻牌成功的情形吗?没有持有相同牌的人(单个人不能比,或者前面没有人翻牌,只能自己一张一张翻),移动一格:

ababac









ababac

c ! = a , 即arr[5] != arr[0],然后再拉开就没有交集了,k=0,---> next[5] = 0


我们现在总结下这个过程。知会的过程其实是首尾比对的过程。怎么比对才能最快知道首尾到底有多少对应相同持牌人?这里可以看到可以利用翻牌成功人数少一点的情形。比如前5个人翻牌成功,我们可以认为前四个人翻牌成功,后来又多一个人:

ababa



ababa

我们只需比对最后加入的人和第k=2个人的持牌情况。因为四个人翻牌成功的情况下,前面两人和末尾两个人已经确认是相同持牌人,新加的人在末尾,只要和往后一个人arr[2]比对;如果相同好办,相同持牌人数就多一个。如果不同,那要重合的部分看首尾比对情况了

ababac



ababac


我们把这个过程翻译为代码:

void get_next(char *pattern,int* next,int plen)
{
	next[0] = 0;
	
	
	//k代表i-1个人成功翻牌时,首尾持牌相同人数
	int k=0;
	
		//i代表第i个新加入的人
	for(int i=1;i<plen-1;i++)
	{
		
		//如果新加入的人与第k个持有的牌不相同	
		//为什么和第k个人比对?因为0到k-1共k个人持有相同牌
		//为什么k!= 0,试想黄色背景部分长度为0,还需要考虑拉开多少距离吗?
		//wile循环要处理的是首尾有重合,但是新加入的不等时要拉开多大距离
		while(k!=0 && pattern[i] != pattern[k])
		{
			//为什么是等于next[k-1]呢?因为在新加入的人与第k个人持有不同牌的情况下,我们要决定要拉开多少
			
			//持有相同牌人数为k,那我们可以看当有k个人翻牌成功时的处理情况,这个处理情况记录在next[k]
			
			//也就是这k个人里面看下首尾有没有交集
			k=next[k-1];
			
		}
		
		//跳湖while循环,要么k==0,要么pattern[i] == pattern[k]
		
		//相等很好办,持牌相同人数加1就好
		if(pattern[i] == pattern[k])
		{
			next[i] = k+1;
			
		}
		//k为0,而且又不等
		else{
			
			next[i] = 0;
		}
		
		//这时更新k为第i个人持有相同牌人数
		k= next[i];
		
	}
}

有了提前协商,那我们匹配时就可以利用翻过的牌了。

int kmp(char* pattern,char* source)
{
	int plen = strlen(pattern);
	int slen = strlen(source);
	int *next = (int*)malloc(sizeof(int)*plen);
	memset(next,sizeof(int)*plen,0);
	
	get_next(pattern,next,plen);
	
	int k=0;
	int i;
	for(i=0;i<slen-1;i++)
	{
		//匹配失败,但是已经匹配成功的首尾有交集
		while(k!=0 && pattern[k]!=source[i])
		{
			k=next[k];
		}
		
		
		if(pattern[k]==source[i])
		{
			k++;
			
			//匹配完成
			if(k==plen)
			{
				return i-plen;
			}
		}

	}
	return -1;
	
}


前七个人翻牌成功:已经成功找到了!!!!!!!!





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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值