今天复习了一下KMP算法,发现忘得差不多了,只能说当时理解还是不够,所以整理了一下,换一个思路来理解KMP算法。
KMP算法主要的思路是对pattern串进行预处理,得到一个数组next数组,next[i] 表示 当pattern[i]与目标串target[j]失配时,应将pattern串中的哪个元素直接移动到与目标串target[j]匹配,而不是一个一个向右移去匹配。其原理是,在我们得到next[i]失配时,我们知道next[0...i-1]是匹配的,亦即target[j-i...j-1]是已知的,而预处理阶段target[j]是未知的。
顺着这个思路,我们不妨假设pattern[0...k-1]与target[j-k...j-1]是匹配的,显然k<i,而在这个假设下,我们应该直接将pattern[k]与target[j]对齐。
那也就是说 pattern[0...k-1] = target[j-k...j-1] = pattern[i-k...i-1],这意味着什么,我们不妨看两个例子
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
pattern[i] | a | a | b | b | a | a | b | b | a |
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
pattern[i] | a | a | b | c | d | e | f | a | a |
对第一个例子,有从pattern[0]开始的aabba和从pattern[4]开始的aabba(或者应该说以patter[8]结尾的)
第二个例子则是,从patter[0]开始的aa和以patter[8]结束的aa。
到这里我们可以回顾一下,顺便考虑一些细节。为了在patter[i]失配的时候,我们要找到一个好的匹配位置k,使得pattern[0...i-1]有一个长度为k的前缀和长度为k的后缀相同(这里的前缀后缀都不包括自身)。这里有几个细节需要注意一下。一个是,我们的的匹配位置k是不包含在前缀中的,因为它要与target[j]对齐;另一个则是,我们这里选取的长度k应为所有可行值中最大的,只有这样才不会出现遗漏。
那接下来就是如何求解next数组。
这里我们采用next数组的前缀后缀含义来理解:next[i] = k,k是pattern[0...i-1]相同前缀后缀的最大长度(这里的前缀后缀都不包括自身)
不如先考虑一下初始情况
(1)i=0 时 next[0] = -1
当pattern[0]与target[j]失配时,我们应将pattern整体右移一位,可以视为我们将pattern[-1]与target[j]对齐
(2)i=1时 next[1] = 0
当patter[1]与target[j]失配时,显然我们应将patter[0]与target[j]对齐
下面我们来看i>0的一般解
此时pattern[0...i-1]匹配,而pattern[i]失配,考虑pattern[0...i-1]
(1)如果pattern[i-1] = pattern[next[i-1]]
显然此时 next[i] = next[i-1]+1
(2)如果pattern[i-1] != pattern[next[i-1]]
我们先来看一个例子
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
pattern[i] | a | b | b | a | a | b | b | a | b | d |
若此时i=9,可以知道next[i-1] = next[8] = 4,而pattern[8] != pattern[4]
抽象一点来看这个问题,pattern[8] 与 pattern[4] 不相同时,可以肯定的是,next[9] < next[8](我们所求的那个前缀必定短于4)。也就是说,我们所求的那个前缀,也是pattern[0...next[8]-1]的一个前缀;而我们所求的前缀对应的后缀,是pattern[8-next[8]...8]的后缀。我们注意到,如果pattern[next[8]] = pattern[8],我们所求的那个后缀,也会是pattern[0...next[8]]的一个后缀。综上,如果pattern[next[8]] = pattern[8],我们所求的pattern[0...8]的前缀,也就是pattern[0...next[8]]的前缀,而这里就是不断递归减小问题规模。
接下来我们考虑一下更实际一点的问题。
第一个问题是,我们不能真的改变pattern中的值。
这个问题很好解决,例如例子中,我们只要每次比较都使用pattern[8]作比较即可,详细可看代码。
第二个问题是,递归的边界。
显然,当我们发现pattern[i-1] = pattern[next[i-1]]时可以退出。另一个边界则应该是k=-1,也就是说pattern[0]都不等于pattern[i-1],前缀长度为0
理解了上面的东西,我们就可以写求next数组的代码了,这里我就直接贴一份书上的代码了,当然里面涉及到了一点优化,代码也更整洁(也更难懂一点)
int *findNext(String P)
{
int i=0;
int k=-1;
int m=P.length();
assert(m>0); //若m<=0则退出
int *next = new int[m];
assert(next !=0) //申请空间失败则退出
next[0] = -1;
while(i<m) //循环中计算的是P[0...i-1]的最大相同前缀后缀长度(i-1是相对于next[i]赋值表达式中的i来说)
{
while(k>=0 && P[i]!=P[k])
k = next[k];
k++;
i++;
if(i==m) break;
if(P[i]==P[k])
next[i] = next[k];
else next[i] = k;
}
}
int KMPStrMatching(const String &T,const String &P,int *N)
{
int i=0;
int j=0;
int pLen = P.length();
int tLen = T.length();
if(tLen<pLen)
return -1;
while(i<pLen && j<tLen)
{
if(i==-1 || P[i]==T[j])
{
i++;
j++;
}
else i = N[i];
}
if(i>=pLen)
return j-pLen+1;
else
return -1;
}
(如果您发现了任何的问题,请务必告知我,十分感谢)