您的位置:首页 > 其它

也谈KMP算法

2013-01-25 01:49 363 查看
看毛片算法,念书时学过,几年下来也就只记得个名字了。前几天偶然遇到,顺道捡起来摸一摸,还是那手感…… ^_^

 

言归正传,这是个模式匹配算法(好吧,正传第一句居然是废话~)。谈它之前,先说说最简单直观的模式匹配算法——朴素匹配。

 

朴素匹配,就是不动脑子去匹配的算法:

 

源串S[0…n-1]  模式串P[0…m-1] 很显然,n > = m

 

源串和模式串分别有一个指针标记,

对指针指向的源串中每一个位置[0…n-m](n-m向后的不用考虑,剩余长度都不够m了),模式串都是从第一位开始比对,匹配则两个指针同时后移一位;

如果遇到某一位不匹配,源串中开始比较位置后移一位,模式串仍然从第一位开始继续匹配。

 

直至模式串扫描完毕表示匹配 或者源串扫描完毕,表示不匹配。

 

给一个Java实现

public int StringMatch(String sourceStr, String patternStr) {
char[] source = sourceStr.toCharArray();
char[] pattern = patternStr.toCharArray();

int idxSource, idxPattern;

// for each i, check if source[i~...i+m-1] matches pattern[0~...m]
for(int i = 0; i <= source.length - pattern.length; i++) {
idxSource = i;
idxPattern = 0;
while(idxPattern < pattern.length
&&source[idxSource] == pattern[idxPattern]) {
idxSource++;
idxPattern++;
}

if(idxPattern == pattern.length)
return i;

}
return -1;
}


简单分析下这个算法,由于外层循环是扫描源串S[0…n-m],内层循环最坏要扫描整个模式串P[m],故算法是O(nm)的。

 

朴素算法里,比如某趟从源串的i = 4,模式串j=0位置开始比对,i和j依次后移,到了i=7 j=3时发现不匹配了,那么下一趟 i又回到5,j继续从0开始匹配下去……

我们发现,源串的指针一直在做前后的“折返跑”(哦,好吧,行话叫“回溯”),有没有办法减少甚至消除此类回溯从而提高效率呢? 额,设问句问出来了,答案当然是有! 而且是本篇猪脚——KMP算法就是基于这种想法而提出的,它消除源串的回溯,是最坏情况O(n)的一个匹配算法!

 

具体说KMP,就是借助一个辅助数组,在匹配过程中一旦出现不匹配,源串指针不用回溯,直接移动模式串继续匹配,直到结束。

 

(注意,由于有形形色色的辅助函数,原理虽然一样,但是边界条件各有不同。各位童鞋如果考试遇到了,请务必按照自己教材的辅助函数计算方式,不然答错了概不负责哈&#**%¥…… 当然话说回来,作者一直认为,数据结构&算法分析啥的考试,让学生手动模拟是很傻很天真的…… 额 貌似又跑偏了)

 

算法思路:

 

如果 S[i-j...i] P[0…j] 在P[j] 失配了,即之前的S[i-j…i-1] = P[0…j-1]  *

但 S[i]<>P[j]

为了达到S串不回溯,需要把模式串P向右移动到某位置(比如K),继续比较S[i] 和 P[k]

 

但是这有一个前提,必须满足P[0…k-1] = S[i-k…i-1] = P[j-k…j-1]  (红色部分是由*式得到的), 否则在P[j]之前的部分就已经不匹配了

 

整理下,就是说如果 S[i-j...i] P[0…j]
在P[j]
失配了,我们希望得到一个值K,可以让我们的匹配从S[i]和P[k]继续尝试下去

 

而K的求法,就是满足P[0…k-1] = P[j-k…j-1]的最大k ( k < j)

 

预定义边界条件 aux[0] = -1 ,如果P[0]失配,S串指针直接后移一位。

 

我们的辅助数组aux
定义就是上面的蓝色粗体字,再强调一遍

 

aux[j] = k 表示如果 S[i-j...i] P[0…j]
在P[j]
失配了,我们可以从S[i]和P[k]继续匹配下去。

那么如果S[i] 和 P[k] 还不匹配呢? 再看看上面的定义,P[k]失配, 我们是不是要尝试S[i] 和 P[ P[k] ] ?

没错,重复这个过程,直到遇到S[i]可以和某个P[#] 匹配,或者 #=0还不匹配了,S串指针后移到i+1, P串再重头来过。

(注意,初涉KMP会导致习惯性头晕,个人建议头晕时反复咀嚼蓝色粗体字,效果不错哦~ 实际上,我写这篇的时候也嚼了好几遍的,嚼着嚼着就不晕了 :P)

 

有了aux数组,我们的匹配就可以在源串中一路向西了,哦不,是一路到底哈……

下面给出一个KMP的匹配实现,计算aux数组后面会谈到

(这里注意,预定义 aux[0] = -1)

public int StringMatchKMP(String sourceStr, String patternStr) {
// initialize auxiliary array
int [] auxKMP = auxInitialNonRecursive(patternStr);
//		int [] auxKMP = auxInitialRecursive(patternStr);

char[] source = sourceStr.toCharArray();
char[] pattern = patternStr.toCharArray();

for(int i = 0; i <= sourceStr.length() - patternStr.length(); i++) {
int idxPattern = 0;
while(idxPattern < pattern.length && idxPattern >= 0) {
if(source[i] == pattern[idxPattern]) {
i++;
idxPattern++;
}
else { // mismatch in source[i] and pattern[idxPattern]
idxPattern = auxKMP[idxPattern];
}
}
if(idxPattern == pattern.length)
return i - pattern.length;
}

return -1;
}


下面说说计算aux数组(
4000
其取值仅与模式串本身有关)

再嚼一次:aux[j] = k 表示如果 S[i-j...i] P[0…j]
在P[j]
失配了,我们可以从S[i]和P[k]继续尝试匹配。

K的求法,就是满足P[0…k-1] = P[j-k…j-1]的最大K

 

第一种方法,根据 “满足P[0…k-1] = P[j-k…j-1]的最大K”求K

即对每个1 <= j < m  求满足条件的k,如果没有,返回0

(神码?为啥返回0?  头晕嚼一嚼, aux[j] = 0 是不是表明, P[j]失配,我们用P[0]尝试)

 

同样,Java实现如下

private int[] auxInitialNonRecursive(String patternStr) {
char[] pattern = patternStr.toCharArray();

int [] aux = new int[patternStr.length()];
aux[0] = -1;

for(int j = 1; j < patternStr.length(); j++) {
// check k from j-1 to 1, does p[0...k-1] matches p[j-k...j-1] ?
int k = j - 1;
while(k > 0) {
if(checkMax(k, j, pattern)) {
aux[j] = k;
break;
}
k--;
}
if(k == 0)
aux[j] = 0;
}
return aux;
}

/** check if pattern[0...k-1] matches pattern[j-k...j-1]*/
private boolean checkMax(int k, int j, char[] pattern) {
int m = 0;
int n = j - k;

while(pattern[m] == pattern
&& m < k) {
m++;
n++;
}

return (m == k);
}

 

第二种方法,这也是大多数算法书上使用的,递归计算辅助数组

初值还是 aux[0] = -1

假设我们已知 aux[j-1] = k,现在要计算 aux[j]

还是从定义出发, aux[j-1] = k 表示 当P[j-1]失配时,应该移动P串到P[k]开始尝试匹配,而且P[0…k-1] = P[j-k-1…j-2] (这个等式换个角度看其实就是P串自身和自身在做匹配了,当然看不出来也木有关系的)

 

那么,如果 P[k] = P[j-1] 那么P[0…k] = P[j-k-1…j-1] 对比下前面定义,等价于 aux[j] = k+1(这说明当P[j]失配时,移动P串到P[k+1]开始尝试匹配,而且P[0…k] = P[j-k-1…j-1] )

如果 P[k] <> P[j-1]
,相当于 P[k]失配了, 我们令 k' = P[ P[k] ] ,当然也有P[0…k'-1] = P[j-k-1…j-2],

同样,如果P[k'] = P[j-1] 那么P[0…k'] = P[j-k-1…j-1], 等价于 aux[j] = k'+1

重复上面的过程,直到找出aux[j] 或者 某个k = -1,此时 aux[j] = 0

 

递归实现如下

private int[] auxInitialRecursive(String patternStr) {
char[] pattern = patternStr.toCharArray();

int [] aux = new int[patternStr.length()];
aux[0] = -1;

for(int j = 1; j < patternStr.length(); j++) {
int k = aux[j - 1];

while(k >= 0 && pattern[k] != pattern[j - 1])
k = aux[k];

aux[j] = (k < 0) ? 0 : k + 1;
}

return aux;
} 

可以借助平摊分析,得出KMP算法是O(n)的时间复杂度,至此,KMP算法就算谈完了
总结一下,
KMP分两个部分
求辅助数组 &
应用辅助数组进行串匹配
通过使用辅助数组,KMP消除源串的回溯,(严格说,还是有原地踏步匹配的,但没有回头重复匹配的);
模式串是有回溯的——但未必每次都要回到第一位开始匹配,这个取决于辅助函数的返回
 
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  算法 KMP