新浪微博 Github

KMP算法总结

 

一、KMP算法介绍

 

KMP(Knuth、Morris、Pratt三人设计的算法);

KMP算法主要用于模式匹配,简单地说就是字符串的匹配,比如A="abc",B="b",问:B是否是A的子串,此时就需要用到KMP算法;因为普通的算法效率太低;

而KMP可以做到O(m+n)的线性时间;

 

二、普通模式匹配算法

 

模式匹配算法简单地说就是给定两个字符串A、B,看是否B是A的子串,Java 中String类的indexOf()就是实现这个功能;

 

普通模式匹配算法思想:

比如有两个字符串A:"ababc",B:"abc";

第一步:初始化  i=1,j=1;(A[0]与B[0]可以空着或者存放字符串的长度);

第二步:循环遍历A和B,如果A[i]==B[j],则i++,j++;

第三步:当跳出循环时,判断是否B是A的子串;

步骤:

1. i = 1,j=1;

 

2.因为A[i]==B[j],因此i++,j++;

3.因为A[i]==B[j],因此i++,j++;

4.因为A[i]!=B[j],所以 j还原为1,i还原为 i-j+2;

5.因为A[i]!=B[j],所以 j还原为1,i还原为 i-j+2;

6.因为A[i]==B[j],因此i++,j++;

7.因为A[i]==B[j],因此i++,j++;

8.因为A[i]==B[j],因此i++,j++;

9.因为i和j同时越界,说明在最后匹配,所以B是A的子串;

 

从上面的步骤中可以看出:

普通的模式匹配算法是效率低下的,有许多步骤都是冗余的,比如第5步:A[2]和B[1]的比较,因为在之前,我们已经比较过:A[2]=B[2],B[1]!=B[2],所以A[2]!=B[1],因此这步就是多余的。

算法如下:

 

private static int indexOf(String a, String b) {
		//1.定义两个指针,分别指向a和b
		int i = 0;
		int j = 0;
		//2.遍历
		while(i<a.length()&&j<b.length()){
			if(a.charAt(i)==b.charAt(j)){
				i++;
				j++;
			}
			else{
				i = i-(j-1);
				j = 0;
			}
		}
		/*
		 * 匹配
		 * 1.匹配a的中间字符串i<a.length()
		 * 2.匹配a的最后字符串(i==a.length()&&j==b.length())
		 * */
		if((i==a.length()&&j==b.length())||i<a.length()){
			return i-b.length();
		}
		else{
			return -1;
		}
	}


 

三、KMP算法

 

KMP算法主要思想:i不用回朔,只需要不断递增即可,只需要j回朔即可;

 

回朔j的规则:

(1)将前面的已经匹配的字符串取出来,找出一个子串,使得此子串既为匹配的字符串的最长前缀也是最长后缀;比如:

匹配的字符串为"ababa",可以看出,"a"、“aba”既是前缀也是后缀,但是“aba”长度较长,所以 j'=“aba".length();

用数学语言表达就是:

当A[i-j+1...i]=B[1...j]且A[i+1]!=B[j+1],则需要调整j,使得重新A[i-j+1....i]=B[1...j];

 

而我们又将回朔j的这个过程用一个数组进行预先计算,记作p[],p[j]表示当有j个字符串已经匹配,但是第j+1个字符不匹配时回朔后的j'的新值;

比如前面的例子p[5]=3;因为"ababa"的长度为5,回朔后的j=3;

 

比如如下例子:

A[1...5]=B[1...5],A[6]!=B[6],i=6,j=5,因此我们需要回朔j(根据预先算好的p[]数组,回朔就是 j=p[j]);

回朔原理:公共的部分为“ababa”,我们可以从此字符串中看出:"aba"是最长的前缀和后缀,因此我们可以进行如下图的变换,使得j=3;

 

A[3...5]=B[1...3],但是A[6]!=B[4],i=6,j=3,因此继续回朔,使得j=1;如下图所示

 

 

A[5]=B[1],但是A[6]!=B[2],i=6,j=1,所以继续回朔,如下图所示,使得j=0:

 

 

因为j=0,所以不能继续回朔,又因为i索引到了A字符串的尾端,但是j仍然不是B的尾端,因此说明B不是A的子串;

 

 

 

KMP算法如下:

/**
	 * O(m+n)  平摊分析
	 *  
	 * @param a 表示文本字符串
	 * @param b 表示模式字符串
	 * @return
	 */
	public static int KMP_indexOf(String a,String b){
		int n = a.length();	
		int m = b.length();
		//变成字符数组的原因是因为我们需要从索引1开始记录数据,比如a = "aba",则cha = {' ','a','b','a'};
		char cha[] = (" "+a).toCharArray();		
		char chb[] = (" "+b).toCharArray();
		int j = 0;	//b的指针
		int[] p = computePArray(chb);		//根据b字符串预先计算出p数组
		for(int i=1;i<=n;i++){					
			while(j>0 && chb[j+1]!=cha[i]){	//j值最多只能减少m次,通过把m次摊还给n次for循环		
				j = p[j];				//回朔
			}
			if(chb[j+1]==cha[i]){		
				j++;
			}
			if(j==m){			//j已经匹配到了尾端,所以全部匹配
				return i-m;
			}
		}
		return -1;
	}


	private static int[] computePArray(char[] chb) {
		int[]p = new int[chb.length+1];
		p[1] = 0;
		int j = 0;
		for(int i=2;i<chb.length;i++){
			while(j>0&&chb[j+1]!=chb[i]){
				j = p[j];
			}
			if(chb[j+1]==chb[i]){
				j++;
			}
			p[i] = j;
		}
		return p;
		
	}


 

预先计算p数组的规则:

 

举个例子,比如B=“ababac”,

1.初始化p[],并且将p[1]=0 ,i=2,j=0;

2.因为B[2]!=B[1],所以p[2]=0,i=3,j=0;p[2]=0;

3.因为B[3]=B[1],所以 j++,即j=1,i=4;p[3]=1;

4.因为B[4]=B[2],所以j++,即j=2,i=5;p[4]=2;

5.因为B[5]=B[3],所以j++,即j=3,i=6,p[5]=3;

6.因为j>0,并且B[6]!=B[4],所以j=p[j]=2;

7.因为j>0,且B[6]!=B[2],所以j=p[j]=0;

8.因为j=0,并且B[6]!=B[2],所以p[6]=0;

 

 

参考文献:

http://www.matrix67.com/blog/archives/115/ 这篇文章写的很不错;

 

此文的实现方法也很好:


public static void computePArray(String T,int p[]){
		T = " "+T;
		int j=0;
		p[1] = 0;
		for(int i=2;i<p.length;i++){
			while(j>0&&T.charAt(j+1)!=T.charAt(i)){
				j = p[j];
			}
			if(T.charAt(j+1)==T.charAt(i)){
				j++;
			}
			p[i] = j;
		}
	}


 

posted @ 2012-02-25 12:14  xiazdong  阅读(214)  评论(0编辑  收藏  举报