KMP算法详解
为什么需要使用KMP
KMP是用于匹配字符串的,也就是在主串s中的,寻找能够完全匹配模式串t的作用。如果我们采取暴力的方式,可以对主串中的每一个字符作为起点,然后依次匹配模式串中的字符,若遇到失配的情况,则将主串移动到当前起点的下一个字母,模式串移动到第一个字母重新进行以上步骤。显然,这样的方法是不优雅的,需要O(n*m)的时间复杂度。然而,KMP提出的方案能够实现O(n+m)的时间复杂度,其本质在于可以当遇到失配的情况时,主串不必移动到当前起点的下一个字母,模式串也不必移动到第一个字母;而是将主串停留在当前的失配字母,模式串移动到之前的某个位置(这个位置之后会提到)。这样对于主串而言就不存在折返的情况,效率大大提升。
KMP的实例——实际如何运作的
主串即为图中上方的字符串,模式串为"abcac",
可以发现,主串的i是不存在折返的,一直在往前进。
拿第一趟匹配进行分析,我们看到 i=3 和 j=3 出现了失配的情况(这也可以说明i=1和j=1,i=2和j=2是可以匹配的),若此时采取暴力的策略,那么必然是将模式串的第一个字母‘a’与主串的第二个字母‘b’进行匹配,但这是不必要的。因为正如之前我们所说i=1和j=1,i=2和j=2是可以匹配的,又因为模式串中第一个字母‘a’和第二个字母'b'是不匹配的,所以将模式串的第一个字母‘a’与主串的第二个字母‘b’进行匹配的话,必然是失配的。我们猜想,主串与模式串的匹配问题是否可以转换成模式串本身的规律。
进而拿第二趟匹配进行分析,第二趟中的大部分字母都匹配成功了,但在最后一个字母失配了,显然我们不用暴力的方法逐个寻求答案。但是可以从暴力方法中提取一些思想,在暴力的策略中,失配发生时,我们移动模式串->判断->移动模式串->判断...重复着这样的过程,但是如果我们可以针对当前的失配具体情况进行分析,直接能够知道该移动模式串的最大步数,省去那些繁琐的无用的判断(如在第一趟匹配中说的那种情况),那不就提高效率了嘛!
所以问题就变成了:当遇到失配情况时,应该将模式串中的哪一个字母移动过来继续与主串当前失配的字母进行匹配。
KMP原理
next数组的定义
$$ next[j]=\begin{cases} 0,当j=1, \:表示模式串第一个字符匹配失败,直接匹配主串的下一个字符\\ Max(k), 其中的k需满足上述的条件 \\ 1 ,不存在有效的k,直接将模式串中的第一个字母与当前字母匹配\end{cases} $$
next数组的编程实现
- next数组的建立实际上是用归纳的思想编程实现的,即已知 next[1]=0 ,若next[j]=k ,那么next[j+1] = ?。分两种情况:
- 若 tj=tk ,T和t都代表的就是模式串,既然直接相等,那么对于j+1来说,就可以将第j个字母也归入麾下,next[j+1]=next[j]+1=k+1.具体推导可以是这样的,因为next[j]=k, 所以T1~Tk-1=Tj-k+1~Tj-1,又因为Tj=Tk,所以T1~Tk=Tj-k+1~Tj,也就得到next[j+1]=k+1.
若 tj≠tk ,假设next[j+1]=k#,由于必须满足k#之前的字母与j+1之前的字母完全匹配,也就意味着(j+1)-1至少必须与k#-1完全匹配,但是此时处在不匹配的状态tj≠tk。我们就需要寻找某一个k‘,不仅仅满足T1~Tk’-1=Tj-k‘+1~Tj-1,还满足Tj=Tk'.由此我们想到next[next[j]].容易证明,若next[j]满足上述等式条件,那么next[next[j]]必然也满足等式,只是next[j]比next[next[j]]更大,所以更优,我们才会取next[j].那么当Tnext[next[j]]等于Tj了,此时就可以把next[next[j]]拿来用了,也就得到next[j+1]=next[next[j]]+1了。若Tnext[next[j]]依旧不等于Tj,那就继续next外套,直至套到next[1]=0为止。以上就完成了next数组的构建。
next构建C++代码如下,
void get_next(SString T,int next[]){ int i=1;next[1]=0;int j=0; while(i<T.length){ if(j==0 || T.ch[i]==T.ch[j]){ //Tj=Tk,j,k为上文中的字母 ++i;++j; next[i]=next[j]; //next[j+1]=k+1 } else j=next[j]; //回退,next[next[j]] } }
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。