详解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
//}
}