KMP算法,看了也有一天了,感觉这个算法真是相当的难,但是也非常的经典,看了许多关于KMP的博客和《算法导论》才算简单的理解了这个算法,感觉书上讲的KMP 适不适合初学者的,这里我就用通俗的语言说一下我理解的KMP。说道字符串匹配,也就是这一样,给出一个T文本字符串,再给出一个P模式字符串,在T中找到模式第一次出现的位置。真如下面:
二.KMP的由来
这么经典的算法想想也知道肯定不是某个人突然就想出来了,而是根据原来算法的缺点不断改进,发现其中的缺陷,找出规律。最早的字符串匹配算法也就是 朴素算法。此算法是利用双指针每次匹配不成功就回溯到当前开始匹配的地方+1处,继续匹配。如图:
显然,这种算法是很不效率的,用上面的图不方便说明,我们用图下说明:
当匹配不成功时,用肉眼就可以看出来,是没必要从T2(也就是b字符)开始进行比较的,正确的无冗余的地方应该是T3位置。
因为,前两个字符已经匹配成功,并且P[1]!=P[2] ,所以很明显没必要比较一遍'a'=='b'?,所以直接从T3位置匹配,那末是如何发现这种能用肉眼看出来的信息呢?我们下面会讲。
朴素字符串算法因为做一些冗余的回溯,所以时间复杂度为 O((n-m+1)m),(n为T的长度,m为P的长度),当m=1/2n,时,此算法就变成了平方 的级别,所以是很低效的。
这里给出 朴素算法的java代码:
public int strStr2(String haystack, String needle) {
char[]a=haystack.toCharArray();
char[]b=needle.toCharArray();
if(needle=="") return 0;
int i=0,j=0;
for(;i<a.length && j<b.length;){
if(a[i]==b[j]){
i++;j++;
}
else{
i=i-j+1;//下一次开始的位置,就为这个位置不用另一个变量 这是有规律的
j=0;
}
}
return j>=b.length ? i-j :-1;
}
字符串匹配一直以来就被先人研究者,因此又有了,Rabin-karp 算法,有限自动机算法,以及最经典的最快的KMP算法。这里只介绍KmP算法。只给出所有算法的时间复杂度:想研究的可以看一下《算法导论》32章。
三.KMP算法
KMP算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。通过一个辅助函数实现跳过扫描不必要的目标串字符,以达到优化效果。没错,这个辅助函数就是给我们了上面提到的有用信息。KMP算法的大体结构其实就是:实现两次对P字符串的匹配。从而达到m+n的时间。
1.前缀函数next
算法导论中把辅助函数也就是前缀函数用next表示,思路:使模式字符串,与本身进行匹配,next数组记录匹配结果,即到P[i]为止的前缀和后缀的最长公共部分,前缀的范围为1——(i-1),是不能超过i长度的。
可以对照下面的π数组理解这个数组存放的什么。
这里先给求解next函数(π)的伪代码:
当看到这个函数时,大部分人肯定都很快理解了当P[K+1]==P[q]的时候,这就是前缀和后缀匹配成功的时候,继续前缀加一个,后缀再加一个字符的判断呗,这里需要注意的是:下表的范围当i==1的时候,π[i]=0,0在数组中是一个无效的数字(仅限于算法导论中,的下标表示方法,若你想使0有意义,可以π[i]=-1,0表示P0位置的字符串是P[i]的后缀),条条大路通罗马,不管哪一种表示,只要注意一下边界就行了。
当P[K+1]!=P[q]的时候,这也是本算法最难理解的部分,也是此算法的精髓,其实当匹配失败的时候,他总是在寻找比匹配失败前的公共长度的最大值,即,当p[i]匹配失败,当前的(p[i-1])最大公共前后缀的长度为 k:
p[1]------p[k]与 p[i-k]------p[i-1]是相同的,既然p[i]匹配失败所以之前的 最大长度 不能用了,所以要找一个同样是 p[1]开头 p[k]结尾的字符串,所以与k位置相同的字符就是 π[π[k]] 位置的字符,可以对照上面的图进行理解。这里给出一个例子。
不妨设模式串Pattern = "a b c c a b c c a b c a"
---------此时满足条件while( LOLP>0 && (Pattern[LOLP] != Pattern[NOCM-1]) )-------------
while语句中的执行
LOLP = 0; NOCM = 12; LOLP = 1; PrefixFunc[12] = 1;
LOLP = PrefixFunc[LOLP];
既然辅助函数已经完成,那么就可以利用辅助函数去在T中寻找最快最有效的偏移,你可能会问仅凭对P的自身匹配就能找出来,最有效的偏移位置? 没错,是的,因为P与T已经部分匹配成功,匹配成功的部分与T中的那部分是一样的字符串。图下说明:
上图匹配到 D与空格的时候 ,匹配失败那么 下一个有效的开始匹配的偏移就是:
s=q-π[6]=6-2=4 q :当前已匹配的最大长度。
其实本质上就是寻找 下一个位置,后面那个位置与P的前缀一致。下面给出伪代码:
四 . KMP java代码
public int strStr(String T,String P){
if(P=="")
return 0;
char t[] = T.toCharArray();
char p[] = P.toCharArray();
int []next = next(P);
int q=-1;
for(int i = 0;i<t.length;i++){
while(q>=0 && p[q+1]!=t[i])//开始迭代用于当已经有成功的匹配时,迭代寻找最大的
q=next[q]; //若不加q>=0,若第一个字符不相等会越界错误
if(p[q+1] == t[i])
q++;
if(q==p.length-1)
return i-q;
}
return -1;
}
/**
* KMP算法的一个辅助函数,next也称为 是前缀函数,求p[i]的
* 最大公共前缀和后缀的长度
*/
private int [] next(String p){
char s[] = p.toCharArray();
int []next = new int[s.length];
next[0] = -1;//与空没有公共部分
int j = -1;
for(int i=1;i<s.length;i++){
//这个while循环是KMP最经典的部分也是最难理解的部分
while(j>=0 && s[j+1]!= s[i]) j=next[j];
if(s[j+1]==s[i])
j++;
next[i]=j;
}
return next;
}