KMP

字符匹配问题。

因为项目需要,在字符串中查找是否有某段特定的字符出现,想起前段时间看的KMP算法,于是总结一下以免忘记了。

例如:字符串s: BDABCDABACD. 模式串p: ABA,因为s中有一小段字符串与模式串ABA相同,在s的第6位(初始为0),否则查找失败。

给s和p分别设置一个指针i,j = 0

一般的匹配过程是:一开始

BDABCDABACD

ABA

发现s[0]和p[0]不匹配。则将p向右移动一位,即i++,j不变。

BDABCDABACD

  ABA

发现此时还是不匹配,则i++,j不变

BDABCDABACD

     ABA

此时i=2,j=0,发现s[2] = p[0] = A,则i++,j++,变成了i=3,j=1。发现s[3] = p[1] = B仍然匹配,则i++,j++,变成了i=4,j=2。但是此时的s[4] = C 就不等于 p[2]=A了。这时j重置为0,i=i-(j-1)=3.(因为之前发现第一次匹配是在i=2的情况,但是发现不匹配,所以i就要达到先前i++的效果,于是就从i=2变到了i=3了)

重复以上过程,发现在i=6的时候又出现了第一次匹配,即s[6] = p[0] = A。就是下面这种状态

BDABCDABACD

               ABA

那么此时执行i++,j++ ,i就变成了7,j变成了1,发现s[7] = j[1] = B, 则再执行i++,j++,发现s[8] = j[2]=B,此时j到头了,于是就匹配了。返回i-j,就代表文本串开始匹配的位置。

这个算法通过遍历整个字符串找匹配,时间复杂度为O(m*n)。(假设m为|s|,n为|p|)比较慢,在扫描s的时候有些是元素是可以直接跳过的,比如在i=2,j=0的时候第一次匹配,到了i=4,j=2的时候适配了,此时i=i-(j-1)=3,回到了指向B的位置,去尝试和p[j=0]去匹配,但是此时肯定会发生失配,因为s[3]=p[1],但是p[0]不等于p[1],所以此时根本不用比就知道了,此步是trivial的。于是就有了下面的KMP算法。


KMP算法是由Knuth, Morris, Pratt这三位大牛于1977年发表的,其中第一位作者就是The art of computer programming的作者,现代计算机鼻祖。

思路:

当匹配到i,j位置的时候,如果j=-1或者匹配成功,s[i]=p[j]的话,i++, j++。

如果失配,即s[i]!=p[j],此时另i不变,j=next[j]。这样做的效果是,模式串p相对于文本串s向右移动了j-next[j]位。


What the hell is next array??

next数组,代表当前字符之前的字符串中(不包括当前字符),有多大长度的相同前缀后缀。例如,next[j] = k,说明在j之前的字符串中有最大长度为k的相同前缀后缀。

本来当失配的时候j回溯到0的,但是现在j不用了,只需要回到next[j]=k的位置就好啦,因为前面的字符串已经匹配了,但是注意这时i是不动的,从头到位都是递增的状态。

然后再拿s[i]去和p[j]尝试匹配。


什么是相同前缀后缀

对于一个p = p_0, p_1, p_2,…,p_j的模式串,如果有p_0,p_1,…p_k = p_j-k, p_j-k-1,…,p_j,则此时next[j]=k+1.

例如模式串ABA,各个字串是

                前缀          后缀           最大公共元素长度

A              空               空                  0

AB            A                B                   0

ABA          A,AB        A,BA               1

所以模式串各个字符的前缀后缀的公共元素的最大长度表

字符   A     B      A

          0      0      1

因为我们看的是失配的字符之前的字符串的最大相同前缀后缀,所以干脆定义一个next数组,next数组等于最大长度表右移1位,然后令首位为-1,另next[j]=k即可。

所以:

字符 

j          0      1     2

字符   A     B      A

next   -1      0      0

private static void getNext(String p, int[] next){
	int plen = p.length();
	next[0] = -1;
	int k = -1;
	int j = 0;
	while(j<plen-1){
		//p[k]表示前缀,p[j]表示后缀
		if(k==-1 || p.charAt(j) == p.charAt(k)){
			++k;
			++j;
			next[j] = k;
		}
		else{
			k = next[k];
		}
	}
}

private static int kmpSearch(String s, String p, int[] next){
	int i = 0;
	int j = 0;
	int slen = s.length();
	int plen = p.length();
	while(i<slen && j<plen){
		if(j==-1 || s.charAt(i)==p.charAt(j)){
			i++;
			j++;
		}
		else{
			j = next[j];
		}
	}
	if(j==plen) return i-j;
	else return -1;
}

时间复杂度,扫描s需要i=0 to m,所以O(m),扫描p需要j=0 to n,所以O(n),所以总的running time = 0(m+n)



点赞