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]\)的意义就是:

\[next[i]=\max\{j\},j<i,A[i-j+1\,\,to\,\,i]=A[1\,\,to\,\,j] \]

\(f[i]\)的意义就是:

\[f[i]=\max\{j\},j\le i,B[i-j+1\,\,to\,\,i]=A[1\,\,to\,\,j] \]

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
        //}
    }
posted @ 2019-11-27 19:47  Seaway-Fu  阅读(996)  评论(0编辑  收藏  举报