一、解决问题:
字符串A(主串):b d d c a b d a a d b d a c a
字符串B(模式串):b d a c
判断字符串A是否包含字符串B 并且找出第一次出现的位置。
对于这个问题比较传统的做法就是遍历两个字符串:
public int test() {
char a[] = {'b', 'd', 'd', 'c', 'a', 'b', 'd', 'a', 'a', 'd', 'b', 'd', 'a', 'c', 'a'};
char b[] = {'b', 'd', 'a', 'c'};
for(int i=0; i<a.length; i++) {
int j = 0;
while (j<b.length && i+j<a.length && a[i+j]==b[j]) {
j++;
}
if(j==b.length) {
return i;
}
}
return -1;
}
可以看出这种写法是遍历了A字符串,依次和B对比,如果发现不符指针i+1继续比较。最坏情况如:
A=aaaaaaaaab , B=aaaab
设A的长度为n,B的长度为m最终将比较n-m趟,每趟比较m次,时间复杂度为O(n*m)。
大牛们哪能忍受这么傻的操作,于是KMP算法就此诞生!
二、KMP算法的原理
上面的示例可以看出,当比较到第五个元素时A[4]!=B[4],按照上面的算法这时就应该把A后移一位,用A[1]-A[5]依次和B[0]-B[4]比较,然后发现比到最后一位发现不匹配又接着重复一遍上面的操作。这时大佬们就说了:
既然 A[0]-A[3]==B[0]-B[3] 并且 B[0]-B[2]==B[1]-B[3] 我们能不能这么移动一下呢:
因为前面的我们已经比较过一遍并且确认相同了,这样我们就可以直接用B[3]和A[4]比较,相当于A没动B向后移动了一位(因为前面四位已经匹配上,并且模式串B的next[4]=3),然后依次重复上面的操作直到结束。注意上面的条件 :A[0]-A[3]==B[0]-B[3] 并且 B[0]-B[2]==B[1]-B[3] ,这是该算法的精髓。
三、程序实现
1、首先我们需要计算模式串B的next数组,也叫前缀数组,next数组用来决定当模式串B和主串A不匹配时,模式串指针移动到哪。
上面的主串A和模式串B时为了说明上面的为题定义的,其实有点特殊,这里我们定义一组新的:
主串A:a b a c b c a b a b a b b c b c
模式串B:a b a b a b b
计算模式串B的next数组:
手动:
在最前面加一个-1得到:next[] = {-1,0,0,1,2,1,2};
如图所示,next数组就是模式串各个位置 最长相同前后缀的长度,其实和主串的匹配时 当某一位置的字符不匹配时我们就是看模式串这个位置之前是否有相同前后缀,有的话就把指针移到前缀的位置。例如:上面的主串A和模式串B 我们在匹配到第四位时发现 A[3]!=B[3],这时我们就得往后移动模式串B来重新匹配了,而next[3]==1 也就是说模式串的前三位最长相同前后缀为1这时我们直接将B的指针置为1就可以继续匹配了。
public int[] getNext(char[] de) { //计算模式串的next数组
int next[] = new int[11];
int i,j,len;
len = de.length;
i = 0;
j = -1;
next[0] = -1;
while(i<len) {// i控制指针不断前进,j只有当遇到相同前后缀的时候才加1,用来记录每个位置相同前后缀的长度
if(j==-1 || de[i]==de[j]) {
i++;
j++;
next[i] = j;
}else {
j = next[j];
}
}
return next;
}
2、EMP算法:
public int kmpTest(char[] A, char[] B, int pos, int next[]) {
int i, j, alen, blen;
i = pos-1;
j = -1;
alen = A.length;
blen = B.length;
while(i<alen && j<blen) {
if(j==-1 || A[i]==B[j]) {
i++;
j++;
}else {
j = next[j];
}
}
if(j>=blen) {
return i-blen;
}else {
return -1;
}
}