在上一篇《KMP算法的Java实现例子以及测试分析》中已经介绍了KMP概念以及原理,这一篇中结合枚举字符串算法比较一下两者的性能,并且给出了KMP、枚举算法的Java实现,以及测试代码与结果,放在这里供大家参考、交流,希望对大家有所帮助。
KMP算法的优势主要有两点:1,无回溯,即对于主串的循环是线性的,也就是说,一直朝前招,指针是递增的,而不会回退;2,对子串加以预处理,从而找到匹配失败时子串回退的位置,即找到匹配失败时的最合适的回退位置,而不是回退到子串的第一个字符。
枚举算法就没有上面两个优点,它是基于回溯的,并且子串匹配失败时直接会退到子串的第一个字符。
下面给出具体例子:
一、三个文件源代码
KMP.java(KMP算法的实现)
源代码为:
package algorithm.kmp;
/**
* KMP算法的Java实现例子与测试、分析
* @author 崔卫兵
* @date 2009-3-26
*/
public class KMP {
static int[] P;
/**
* 对子串加以预处理,从而找到匹配失败时子串回退的位置
* @param B,待查找子串的char数组
* @return
*/
public static int[] preProcess(char [] B) {
int size = B.length;
int[] P = new int[size];
P[0]=0;
int j=0;
for(int i=1;i<size;i++){
while(j>0 && B[j]!=B[i]){
j=P[j];
}
if(B[j]==B[i]){
j++;
}
P[i]=j;
}
return P;
}
/**
* KMP实现
* @param parStr
* @param subStr
* @return
*/
public static void kmp(String parStr, String subStr) {
int subSize = subStr.length();
int parSize = parStr.length();
char[] B = subStr.toCharArray();
char[] A = parStr.toCharArray();
int j=0;
int k =0;
for(int i=0;i<parSize;i++){
while(j>0 && B[j]!=A[i]){
j=P[j-1];
}
if(B[j]==A[i]){
j++;
}
if(j==subSize){
j=P[j-1];
k++;
}
}
}
public static void main(String[] args) {
//回退位置数组为P[0, 0, 0, 0, 0, 0]
kmp("abcdeg, abcdeh, abcdef!这个会匹配1次","abcdef");
//回退位置数组为P[0, 0, 1, 2, 3, 4]
kmp("Test ititi ititit! Test ititit!这个会匹配2次","ititit");
}
}
SubStrFind.java(枚举算法的实现)
源代码为:
package algorithm.kmp;
/**
* 字符串查找(枚举方法查找)
* 在一个字符串中查找是否包含某一子串
* @author 崔卫兵
* @date 2009-3-26
*/
public class SubStrFind {
/**
* 字符串查找(枚举方法)
* @param parStr
* @param subStr
* @return
*/
public static void strFind(String parStr, String subStr) {
int parSize = parStr.length();
int subSize = subStr.length();
char[] B = subStr.toCharArray();
char[] A = parStr.toCharArray();
boolean flag=true;
int times = 0;
int j=0;
int k =0;//k记录父串匹配正确的位置或者匹配不正确的回退位置
//i记录父串的当前比较字符的位置
for(int i=0;i<parSize;i++){
if(B[j]==A[i]){
j++;
//第一次时记录父串回退位置
if(flag){
k=i;
flag=false;
}
}else{
//不匹配时回退位置重置,比较继续进行
i=++k;
j=0;
flag=true;
}
if(j==subSize){
j=0;//匹配时只需把子串回退位置重置,比较继续进行
flag=true;
times++;
}
}
}
}
TimeConsumer.java(性能测试代码)
源代码为:
package algorithm.kmp;
/**
* 字符串查找时间计算(KMP算法与枚举查找法)
* @author 崔卫兵
* @date 2009-3-26
*/
public class TimeConsumer {
private static int times = 1000000;
public static void test(String parStr, String subStr) {
long start = 0;
long end = 0;
System.out.println("父串: "+parStr);
System.out.println("子串: "+subStr);
start = System.currentTimeMillis();
KMP.P = KMP.preProcess(subStr.toCharArray());
for(int i=0;i<times;i++){
KMP.kmp(parStr,subStr);
}
end = System.currentTimeMillis();
System.out.println("Time for KMP: "+(end-start));
start = System.currentTimeMillis();
for(int i=0;i<times;i++){
SubStrFind.strFind(parStr,subStr);
}
end = System.currentTimeMillis();
System.out.println("Time for Enumeration: "+(end-start));
System.out.println("-------------------------------------");
}
public static void main(String[] args) {
test("abcdeg, abcdeh, abcdef!这个会匹配1次","abcdef");
test("Test ititi ititititd! Test ititititd!这个会匹配2次","ititititd");
}
}
二、输出结果
父串: abcdeg, abcdeh, abcdef!这个会匹配1次
子串: abcdef
Time for KMP: 953
Time for Enumeration: 797
-------------------------------------
父串: Test ititi ititititd! Test ititititd!这个会匹配2次
子串: ititititd
Time for KMP: 1109
Time for Enumeration: 1344
-------------------------------------
三、总结
从输出结果中可以看出,对于第一次比较,子串为"abcdef",里面没有任何重复的字符,所以其回退位置数组的每一个元素值全为0,即匹配失败时,自然回退到子串的第一个字符,前文已经说过KMP算法有两个优势:无回溯、回退位置数组,在子串里没有任何重复字符的情况下,回退位置数组就失去作用了,而且还会带来一些额外开销(要计算出回退数组),并且KMP中还有更多的判断分支,这也许就是测试一KMP时间更多的原因所在,对于测试二,KMP自然表现优秀,两个优势都凸显出来了。
对于子串很简单的,譬如10个字符以内,而且又没有字符重复,用枚举字符串查找即可,对于子串较长、重复多时,用KMP较好。
最近在分析Java String的indexOf方法的实现,希望那里有更好的方法。