KMP算法详解
详解KMP算法
KMP算法(也叫做KMP模式匹配算法、模式匹配算法),是一种常用的字符串基本算法。其用途是:在线性时间内判断A串是否为B的子串,并求出A串在B串中各自出现的位置。
暴力求解字符串匹配
在我们还不知道这个世界上有KMP这种东西的时候,我们需要考虑如何暴力匹配两个字符串的包含和被包含关系。
暴力的做法的时间复杂度是\(O(NM)\)的(\(N,M\)表示两个字符串的长度),就是把\(B\)串从\(A\)串的第一个字符开始往后推,每推一位尝试一下匹配。以此类推之后看一看什么时候能匹配到,记录答案。
这个算法的流程可以看下图理解:
(图片引自\(CSDN\)博客)
KMP思路
\(O(NM)\)的复杂度显然太慢了(废话,要不然为什么要学KMP),不能满足我们的需求,那么我们如何来对这种字符串匹配进行优化呢?
我们来回过头对这个问题来进行思考:对于两个字符串的匹配,既然不能一个一个匹配,那就一群一群匹配,即:暴力思路对“字符串匹配”的扩展是一个一个字符地扩,但是我们可以通过一些手段变成一堆一堆的扩。这是我们优化的第一步。
有了这个思路,我们继续往下想:如何能实现由“一个一个扩”到“一堆一堆扩”呢?这里介绍两个概念:(其实算作字符串的一种基础概念,但是因为这里实在用的很多,怕有些读者不清楚,所以拿来重述一遍)
-
前缀:字符串前缀的定义是:从原串开头处开始的连续子串。如:abcd的前缀就分别是a/ab/abc。
-
后缀:类比一下,后缀的定义就是:从原串结尾处向开头处延伸的连续子串。如:abcd的后缀就是:d/cd/bcd。
有了这两个概念,我们就可以发现,其实“一堆一堆匹配”就是对字符串前缀、后缀的匹配,我们可以在匹配之前预先找出来这些字符串有哪些位置出现“成堆”的相等字符,然后对其进行匹配。这样就可以把效率提升很多。
KMP的原理及流程
首先介绍两个数组:\(next[i]\)和\(f[i]\),\(next[i]\)表示\(A\)串(需要和另一个串匹配的小串)前\(i\)个字符构成的子串最大的相同前缀后缀的长度。
例子:
abababaac
在这个长度为9的字符串中,\(next[7]\)表示前7个字符所构成的子串(abababa)中最长的相同前缀后缀,根据前缀和后缀的定义,我们可以发现,前5个字符和后5个字符是相等的,但前6个、前7个字符和后6个,后7个字符都不等了,所以\(next[7]=5\)。
我们把\(A\)串的\(next\)数组的求解过程叫做KMP算法的自我匹配过程。
\(f[i]\)数组表示\(B\)串(进行匹配的长串)前\(i\)个字符构成的子串的后缀和\(A\)串的前缀相同的最大长度。
我们把\(B\)串的\(f\)数组的求解过程叫做KMP算法的异串匹配过程。
用公式理解一下:
\(next[i]\)的意义就是:
\(f[i]\)的意义就是:
next数组和f数组的求解
根据以上对\(next\)数组和\(f\)数组的定义,我们可以发现,当\(f[i]=n\)(\(n\)为\(A\)串长度)的时候,就是\(A\)串在\(B\)串出现的时候,这个\(i\)就是\(A,B\)共同串的结尾。
也就是说,我们只需要想办法求出\(next\)数组和\(f\)数组,就可以完成KMP算法的流程。
\(next\)数组和\(f\)数组的求解是相似的过程,这是由它们定义上的相似性得出的。
以\(next\)数组的求法举例:
\(next\)数组的求解是一个递推的过程。需要好好理解。
在求解\(next[i]\)的时候,我们实际上是借助\(next[i-1]\)的可行解转移的。假设我们现在已经得出\(next[i-1]\)的解,那么对于\(next[i]\),只需要匹配一下\(A[i]\)和对应前缀的下一个字符是否相等即可,假如相等,就可以在\(next[i-1]\)的基础上直接进行\(+1\)。
否则呢?(重点来了)
注意!如果不等的话,并不是直接继承\(next[i-1]\)的值。为什么呢?因为我们在\(i-1\)串的结尾加入了一个新字符\(A[i]\),这导致了当前子串的后缀发生了变化,所以不能再从之前的\(next[i-1]\)推导。
那么,理所应当地,我们应当从前面的那些“合法子串”中寻求最大的那个继续进行匹配。也就是说,既然\(next[i-1]\)不能继承,那我们就尝试继承更小一点的合法串,那么这个更小一点的合法串怎么找呢?答案就是:\(next[next[i-1]]\)。
很惊讶吧?用一个例子说明:
假如\(next[7]=5\),这实际上说明了\(A\)的前\(5\)个字符和从7往前的\(5\)个字符是相等的,那么,对于这个新插进来的字符\(j\),假如它和从5往前的\(j\)个字符一起与\(A[1\,\,to\,\,j]\)匹配的话,那么自然地,从7往前的\(j\)个字符和\(A[1\,\,to\,\,j]\)也是相等的。那么这样的\(j\)最大是多少?就是\(next[5]\)。
KMP算法模板
注:在一些新型的编译器(请原谅笔者不知道具体是什么新型的编译器)下,\(next\)这个词成了\(C++\)的保留字,如果交上去会\(CE\)。(膜拜郭爷\(GXZLegend\)大佬)所以把\(next\)数组变成了\(nxt\)数组,想来不会有什么问题。
模板:(求\(next\)数组)
nxt[1]=0;
for(int i=2,j=0;i<=n;i++)
{
while(j && a[i]!=a[j+1])
j=nxt[j];
if(a[i]==a[j+1])
j++;
nxt[i]=j;
}
模板:(求\(f\)数组)
for(int i=1,j=0;i<=m;i++)
{
while(j && (j==n || b[i]!=a[j+1]))
j=nxt[j];
if(b[i]==a[j+1])
j++;
f[i]=j;
//if(f[i]==n)
//{
//solve
//}
}