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