<span style="font-family: Arial, Helvetica, sans-serif;">package com.algorithm.string;</span>
public class KMP {
private static void nextval(char[] P,int[] next){
next[1]=0;
int i=1,j=next[i];
while(i<P[0]){ //这里与教材中不同
if(j==0||P[i]==P[j]){
i++;
j++;
while(P[i]==P[j]&&j>=1){
j=next[j];
}
next[i]=j;
}else{
j=next[j];
}
}
}
public static int match(char[] T,char[] P,int[] next){
int i=1,j=1;
while(i<=T[0]&&j<=P[0]){
if(j==0||T[i]==P[j]){
i++;
j++;
}else{
j=next[j];
}
}
if(j>P[0]){
return i-P[0];
}else{
return -1;
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
char[] T=new char[]{10,0,1,2,2,4,2,2,2,8,9};
char[] P=new char[]{3,2,2,2};
int[] next=new int[4];
nextval(P,next);
System.out.println(match(T,P,next));
}
}
输出结果:6
先说明一下,本文的基础是严蔚敏的《数据结构》C语言版,字符串数组第0个元素用来存储字符串长度。上面是java实现的KMP修正算法,与教材中不同的是当出现需要修正的情况时,这个程序使用循环结构不停的滑动逻辑模式串,直至P[i]!=P[j],而教材中只滑动了一次。
KMP算法的关键就在于next数组的构造,这个数组的表意非常绕口,很容易造成逻辑上的混乱,这也是KMP难于记忆的原因。next[i]的值代表的实际意义为:模式串中前i-1个字符组成的子串的最长前后缀字符串之后第一个字符的索引。前后缀字符串是我自己取的名字,它表示同时为前缀和后缀的字符串,比如说字符串cbcb中,cb即为前后缀字符串,但是注意,字符串cbcb本身并不是它自己的前后缀字符串,虽然它的确即为前缀也为后缀,这只是一种规定。上面是next数组的一般含义,但是有两个特例,一是当前i-1个字符组成的子串的前后缀字符串不存在时,next[i]=1,二是规定next[1]=0。所以next[i]的取值一共有三种情况,与数据结构教材中的那个数学公式是一致的,这里就不贴那个公式了。以下图中模式串P为例,构造next数组,next[0]我们闲置它,next[1]——next[6]分别为{0,1,1,3,3,4}。构造出next数组之后的匹配过程是很容易理解的,如下图匹配至i=7,j=5时,T[7]!=P[5],只需令j=next[j],继续与T[7]匹配,至于为什么,是一个等式传递性的问题,数据结构教材中都有提到,也比较好理解。
下面主要说一下next数组的构造过程,其实从next数组的定义我们可以知道对于任意一个字符串来说,next[2]一定是等于1的,因为前2-1=1个字符组成的字符串不存在前后缀字符串。而且我们可以证明,next[i]与next[i-1]之间存在数量关系,因此可以用递推的方法求得next数组。如下图所示,next数组的构造过程我们可以想象成两个字符串的匹配过程,分别是逻辑主串P1,逻辑模式串P2,之所以称之为逻辑字符串,是因为P1和P2物理上是同一个字符串——模式串P。假设当前已求得next[i]=3,现在来求next[i+1]。不难想象,由于模式串中前2个字符组成的字符串与第i个字符前面的2个字符组成的字符串(不包括第i个字符)相等,所以若P[3]=P[i],则模式串中前3个字符组成的字符串必与第i+1个字符前面的3个字符组成的字符串(不包括第i+1个字符)相等,故此时前i个字符组成的字符串的最长前后缀字符串长度为3,故next[i+1]=3+1=4。抽象出来,即:若next[i+1]=j+1;若P[j=next[i]]!=P[i],则设j=next[j],要继续比较P[i]和P[j],直至相等或j=0。如下图next[5]=3,现在来求next[6]。我们来比较P[5]和P[next[5]]=P[3],由于它们相等,故next[6]=4。这是原始的KMP算法,这里有一个可以优化的地方,我们知道,当主串T与模式串P匹配时,若匹配到模式串第6个字符失配时,则要拿模式串中第next[6]=4个字符与主串当前字符比较,当P[6]与主串当前字符不匹配时,若P[4]=P[6],显然P[4]也不匹配,故在计算next[6]时,逻辑模式串还要继续向右滑动next[4]=2个单位,此时得到next[6]=2 ,由于P[6]与P[2]相等,还要将逻辑模式串在向右滑动next[2]=1个单位,故next[6]=1。