JAVA语言kmp,KMP算法(一)——java实现

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应该是这样的

a

a

a

a

0

1

2

3

第一个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算法实现起来就会容易地多了

你可能感兴趣的:(JAVA语言kmp)