KMP算法(1)——java实现

这篇博文写的实在是太糟蹋了,请大家移步KMP算法小结,如果未能帮大家解惑,请见谅,并欢迎再新博文下面留言讨论。

 

KMP是比较知名的一个字符串匹配算法。由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现(不明白什么叫同时发现+_+)因此得名KMP算法。

 

首先大家想一下字符串如何匹配?

比如str1 = “BBC ABCDAB ABCDABCDABDE”,想知道这个字符串中是否包含str2 = “ABCDABD”。

逗逼:“这尼玛欺负我不会拼contains吗?”回答正确。但是contains的实现是怎样的?不用看源码,先自己想一下有木有思路。

一个简单直接的思路是:从str1第1个字母开始匹配,如果成功,good,匹配第2个...直到结束或者第i个字母不相等,然后从str2的第2个字母开始匹配....

是的,JDK源码就是这么做的。详见Implement strStr

这个方法是暴力算法,也可以说回溯。一般而言,暴力算法往往都不是最优的。KMP算法即是字符串匹配的优化算法。

本博文主要先讲一下KMP算法为什么比暴力算法更优。

 

在这之前,先给大家普及两个概念:前缀 & 后缀。

拿一个例子来讲:“china”

前缀::“c”,“ch”,“chi”,“chin”

后缀:"a","na",“ina”,"hina"

不知道大家有没有看明白,

所谓前缀就是str.substring(0,n) —— 其中n从1 ~ str.length() - 1(n == 0时,返回一个空串,不算前缀)

所谓后缀就是str.substring(m)——其中m从1 ~ str.length()-1

 

接下来的部分整理自网络日志,感谢原作者。

 

看下面两个字符串

【我们采取Implement strStr中的定义,将上面的字符串称为haystack,下面的称为needle】

B与A不匹配,haystack后移一位,得到:

 

还是不匹配,继续后移:直到:

good,匹配成功,然后匹配下面needle的第2个字符。

B和B,还是相同,一直匹配,直到:

按照我们之前的暴力做法,看到D匹配失败之后,就回溯到:

继而从B往下匹配,相当于haystack的指针往后回溯了。这肯定是对的,但是每次needle不能完全匹配时,都需要回溯,因此最坏的时间复杂度是n*m

 

接下来看下KMP是怎么做到不回溯的

我们继续回到回溯前的这一刻:

虽然匹配到D时,匹配失败了,但是我们知道了之前的ABCDAB是成功匹配的,暴力算法没有用到这些信息。KMP算法即设法利用这些信息,并不能让needle 和 haystack无需回溯。从而提升效率。

 

要做到这一点,需要用到Partial match table——部分匹配表

至于该表怎么求的,一会儿再讲

 

好,再回到刚才的状态

D(下标为 j )与空格(下标设为 i )匹配失败,前面ABCDAB是匹配的,匹配长度为length(本例是6),查表得B(要查失配字母的前一个字母,或者最后一个匹配的字母)对应的value为2,因此我们将needle前移length - 2 位【代码是将 j 的值减去length - 2(本例是4位)】即得到

PS:插播一下上面的移动位数的计算公式
moveStep = partial_match_length  -  table[partial_match_length - 1]

 

看到这里是不是有点明白了,移动之后 空格 之前的AB是匹配好的。

这里KMP算法耍了一个小聪明,首先,我们看到AB是match的字符串ABCDAB的一个前缀,但是也是一个后缀,而这个前缀和后缀是相等的,对于haystack字符串而言,空格之前的AB本来是匹配后缀的,然后,我们将前缀移动过来跟它匹配,有点 x = y && y = z 得到 x = z 的感觉。

解释:x(前缀) = y(后缀) && y = z(后缀与haystack匹配)  =>  x = z(前缀也可以和haystack匹配)

从而无须将指针回溯,肯定有人会问,这样直接跳过会不会漏掉中间的情况,答案当然是否定的。

提示一下,我们当时选:前缀==后缀时,选的是最长的,比如aaaa,满足条件的最长前缀(后缀)是aaa。

 

言归真正,空格 与C 还是不匹配的,match的字符串(AB)长度length == 2,而C前的B在Partial match table中的value为0,因此我们需要将needle前移动length - 0(即2位)得到

 

 

这种情况比较简单,haystack后移:

重复上述步骤,发现:这下发了。。。一直匹配到D才适配,这次needle移多少位呢?且看D的前的B的value为2,match的length = 6.因此前移4位,得到

然后继续往下匹配。找到一个!如果想继续找下去,查看D的value值 == 0,match的length == 7,则移动7位。跟之前的一样。

 

 

Partial match table——部分匹配表

首先来看PMT中的value的意义:

还以刚才的字符串needle为例

A的value为0,表示以字符串A,相等的前缀和后缀中,最大长度为0,显然,单字符压根就没有前后缀

B的value为0,表示字符串AB,它只有一个前缀:A,一个后缀B,不存在相等的前缀,后缀,因此=0

来看第二个B,表示字符串ABCDAB,它有一个前缀AB,同时也有一个后缀AB,长度为2,因此value=2

 

这样讲应该很明白了吧,上文中还举了一个例子“aaaa”

它的PMT应该是这样的

aaaa
0123

第一个a,肯定value为0

第二个a,表示字符串aa,有一个前缀a,一个后缀a,因此value = 1

第三个a,表示字符串aaa,有前缀a,aa。有后缀aa,a,因此相等的最长前缀后缀应该是aa,value=2

 

 

最简单的代码实现如下:

	public static int[] getPartialMatchTable(String s) {
		if (s == null) {
			return null;
		}
		int length = s.length();
		int[] value = new int[length];
		value[0] = 0;
		for (int i = 1; i < length; i++) {
			String tem = s.substring(0, i + 1);
			int prefixEnd = i;
			int suffixBegin = 1;
			String prefix;
			String suffix;
			while (prefixEnd > 0) {
				prefix = tem.substring(0, prefixEnd);//取前缀
				suffix = tem.substring(suffixBegin);//取后缀
				if (prefix.equals(suffix)) {
					value[i] = prefixEnd;
					break;
				} else {
					prefixEnd--;
					suffixBegin++;
				}
			}
		}
		return value;
	}

 有了PMT,KMP算法实现起来就会容易地多了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值