KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出,所以被称为看毛片算法(我可不知道毛片是什么东西)。
其核心思想就是利用模式串的局部对称,在每一次匹配失败后,达到一种玄幻的跳跃,极大加快匹配速度。玄幻如下:
(注:上串为主串,下串为模式串)
当主串与模式串匹配到i=j=4时,发现不匹配,KMP算法下发生了恐怖事件,匹配状态直接变为下图。
我们朴素的字符串匹配方法,匹配到i=j=4时,下一步操作应该是把j置为0,i置为1,继续进行比较。而KMP这一步直接将j置为2,与i=4进行比较。(想一想,为什么?)
这里利用了模式串的首尾子串的对称性。在j=4的b之前的字符串,头两个字符和最后两个字符相同,利用首尾部分的重叠,将头部分覆盖尾部重叠部分,继续之后的比较,这样就跳过了没有必要的比较。(想一想,为什么?)
有的小伙子可能会问:凭啥中间的就没有比较的必要了?万一中间有和头部重合的部分,但是主串和模式串却匹配了,而且并不属于尾部重叠串,你这个部分就有可能出错啊!
回答当然是:不可能!因为重叠若没有延续到结尾,则必然有不相等的部分,看如下栗子。
对于模式串来说,第3、4个字符和第1、2个字符是一样的,但当匹配到d!=a时,将字符串“后移”,使其变为如下情况:
显然是肯定不会匹配的,因为若匹配了,第3、4个ab应属于尾部重叠串的一部分。
所以可以再来康康下面这种情况
下一步操作使情况变为
(想一想,为什么?)
到此KMP算法的大致思想我们应该知道了,感觉代码应该不难写。上代码:
/**
* KMP工具类
*/
public class KMP {
/**
* 用于计算匹配的位置(从头到尾)
* @param str 主串
* @param sub 模式串
* @return
*/
public static int kmp(String str, String sub) {
int i = 0,j = 0;
int[] next = getNext(sub);
while (i < str.length() && j < sub.length()){
if(j == -1 || str.charAt(i) == sub.charAt(j)){
i++;
j++;
}else {
j = next[j];
}
}
if(j == sub.length())
return i-j;
else
return -1;
}
/**
* 用于生成部分匹配表next
* @param sub 模式串
* @return
*/
private static int[] getNext(String sub) {
int[] next = new int[sub.length()+1];
int i = 0, j = -1;
next[0] = -1;
while(i<sub.length()){
if (j==-1 || sub.charAt(i) == sub.charAt(j)){
next[++i] = ++j;
}else {
j = next[j];
}
}
return next;
}
}
当然感觉永远是错的,next又是个what?你没有想错,next就是模式串当前匹配失败后应该“覆盖”他的字符编号。比如下图,c所属的字符编号就为6,记为next[8]=6(c是第7个元素)。
求这个next数组的代码也很匪夷所思。我来讲讲大概过程,看起来应该就一目了然了。
若要求next[13],可知next[12]=6。此时j=6,这就表明1-5和7-11元素重叠。
此时比较sub[6]和sub[12]
若相等,则next[13]=j+1=7。
若不相等,则使j=next[6]。
若next[6]=3,则1、2和4、5处元素重叠,并且由next[12]=6,可得1、2处元素和10、11处元素重叠,此时比较sub[3]和sub[12]
若相等,则next[13]=j+1=4
若不相等,且next[3]=0,就变为首字符和尾字符的比较,若相同,则next[13]=2,若不同,next[13]=1。
最后写个main
public class Main {
public static void main(String[] args){
int res = KMP.kmp("abababbcabca","ababbca");
if (res==-1){
System.out.println("匹配失败");
}else {
System.out.println("匹配位置为"+res);
}
}
}