KMP 模式匹配算法与扩展 KMP

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43959739/article/details/84867201

KMP 模式匹配算法与扩展 KMP 相关

前言

本文导览

KMP 算法简称 Knuth-Morris-Pratt Algorithm (中译:克努特——莫里斯——普拉特操作), 主要用于解决字符串中的模式匹配问题,本文将介绍以下几点

  • 字符串的模式匹配问题
  • nxt 辅助数组的构建
  • KMP 算法的实现

exKMP 算法是对 KMP 算法的一个拓展,使得在同样的时间复杂度内,可以获得更多的信息,本文将介绍以下几点

  • exKMP 所要解决的问题
  • exKMP 算法的实现

最后,我们分析总结两个算法的时间复杂度。

一些符号与术语

【字符集大小】

一个字符串中所有出现的字符的集合叫做这个字符串的字符集,用符号 Σ\Sigma 表示。

【字符串的连接】

对于一个字符串 xx 和另一个字符串 yy,书写方式 xyxy 表示 xxyy 的一个连结(concatenation),即将 yy 接在 xx 的后面。

【前缀与后缀】

对于字符串 ssxxyy,如果 s=xys = xy,那么 yyss 的后缀(符号为 \sqsupset), xxss 的前缀(符号为 \sqsubset

KMP 模式匹配算法

模式匹配问题

在介绍 KMP 模式匹配算法之前,先来介绍字符串的模式匹配问题。

对于文本串 TT 和模式串 PP,如果 0snm0 \leq s \leq n - m,并且 T[s+1..s+m]=P[1..m]T[s + 1 .. s + m] = P[1 .. m],那么称 PP 在文本串 TT 中出现,且偏移量ss。如果 PPTT 中以 ss 偏移出现,则这种偏移为有效偏移,否则为无效偏移

字符串匹配问题就是找到所有 PP 的有效偏移,使得 PPTT 中出现。

有效偏移的例子

从朴素算法开始

采用朴素的算法来求解有效偏移时,我们对每一个可能的有效偏移进行枚举,并从这一位置开始,向后匹配所有的字符,看模式串 PPTTss 开始的部分是否完全一致,不难发现枚举有效偏移的复杂度为 O(T)O(|T|),检查有效偏移的复杂度是 O(P)O(|P|) , 因此这个算法的运行时间为 O(TP)O(|T| |P|)O(n2)O(n ^ 2) 复杂度。

充分挖掘模式串的性质

让我们考察一下朴素字符串匹配的过程,如下图 aa 中所示的局面是正在检查偏移 ss 是不是有效的,当匹配了 q=5q = 5 个字符时, 模式串 PPTT 因为 ac 的差异而失配。朴素算法此时执行 s+1s + 1 并重头来过。但是,我们可以确定 s+1s + 1 不能成功匹配,因为 P[1]P[1]P[2]P[2] 的字符是不同的。这个性质是与文本串无关的,它只由模式串决定。那么我们需要关心这样一个问题:

从第 xx 位失配的 PP ,下一次可能的成功匹配位置在多少位之后?

例如在例子中,我们如果知道在第 66 位失去匹配的字符串应该向后移动 22 位,我们的匹配就可以加速进行。

形式化的说:已经知道了 PqTs+qP_q \sqsupset T_{s+q},我们要找一个长度最小的 PkP_k 满足 PkPqP_k \sqsubset P_q 并且 PkPqP_k \sqsupset P_q。存储这个问题答案的函数记作后缀函数,下文写作 nxt

在这里插入图片描述

nxt 辅助数组的构建

可以利用模式串自身与自身的比匹配来得到这样的信息,如下图所示,采用递推的思想,可以在 O(P)O(|P|) 的时间内处理所有位置的 nxt 值。

程序中的字符串与数组的下标从 00 开始,nxt[i] 表示 PPi 个字符的最长公共前后缀的长度,也表示当匹配到第 i 位时,如果失配应从 PP 串的第 nxt[i] 为开始进行匹配,这样看上去使得两者的功能有些错位,但是却给我们的匹配带来方便。

inline void getFail(char *P, int *nxt)
{
    int m = strlen(P);
    nxt[0] = 0; nxt[1] = 0;
    for(int i = 1; i < m; i++)
    {
        int j = nxt[i];
        while(j != 0 && P[i] != P[j]) j = nxt[j];
        nxt[i + 1] = P[i] == P[j] ? j + 1 : 0;
    }
}

算法的思想可以说是用自己匹配自己,具体步骤可以参考下面的图示。

在这里插入图片描述

通过 nxt 辅助数组来进行模式匹配

匹配 PPTT 的过程是用别人来匹配自己,它看上去和构造 nxt 没有什么不同。

inline void find(char *T, char *P, int *nxt)
{
    int n = strlen(T), m = strlen(P);
    getFail(P, nxt);
    int j = 0;
    for(int i = 0; i < n; i++)
    {
        while(j != 0 && P[j] != T[i]) j = nxt[j];
        if(P[j] == T[i]) j++;
        if(j == m) \\ready_find
    }
}

具体的做法是,当两个串的元素相等时,步进匹配进程,否则就根据 nxt 辅助函数的值,直接跳转到下一个有可能成功匹配的位置。理解起来并不困难,读者可以自己构造两个文本串模拟来加深理解。

nxt 数组的其他性质

有时,KMP 算法并不会常常出现在题目之中,但是经常能看到 nxt 数组的身影,读者也不难发现,我详细介绍了关于 nxt 数组的知识。下面介绍两个 nxt 数组的性质。

  • 根据 nxt 数组的性质,长度为 nn 的字符串的最长公共前后缀(不包括自己)的长度为 nxt(n)nxt(n)
  • 根据上一条性质,我们可以发现,一个串可以表示成一个子串的循环,这个子串的最短长度为 nnxt(n)n-nxt(n),下面这张图帮助你的理解。

在这里插入图片描述

exKMP 扩展 KMP 算法

exKMP 算法是对 KMP 算法的扩展,它不仅可以实现 KMP 的功能,还可以得到更优秀的字符串信息。在开始下面的阅读之前,需要读者已经掌握上面介绍的 KMP 算法。

问题的提出

exKMP 算法要求我们求解出一个关于字符串 TTPP 的一个数组 extendextend 表示对于 TT 的每一个后缀,与 PP 串的最长公共前缀的长度,即,从第 i 为开始所能匹配到的最远位置。

不难发现,这个问题是 KMP 解决的问题的超集,当 extend[i] = length(P) 时,ii 就是一个有效偏移,就是 KMP 算法求解的对象。

尝试解决

我们定义三个工具来作为我们解决问题的辅助。

  • 位置标记 aa
  • 位置标记 pp
  • 辅助数组 nxtnxt

其中 pp 是从 aa 位置开始,第一个失去匹配的位置。nxtnxt 数组表示每个模式串 PP 的后缀与模式串 PP 的最长公共前缀的长度。通过 nxt 来求解 extend 有如下三个情况:

第一种情况:如果 i + nxt[i - a] < p,如图,标记的区间长度相同,根据 nxt 数组的定义,此时 extend[i] = next[i - a]

第二种情况:如果 i + nxt[i - a] == p,如图,T[p] != P[p - a]P[p - i] != P[p - a],但 T[p] 有可能等于 P[p - i],所以我们可以直接从 T[p]P[p - i] 开始往后匹配,加快了速度。

第三种情况:如果 i + nxt[i - a] > p,那说明 T[i...p]P[i-a...p-a] 相同,注意到 T[p] != P[p - a]P[p - i] == P[p - a],也就是说 T[p] != P[p - i],所以就没有继续往下判断的必要了,我们可以直接将extend[i]赋值为 p - i

在这里插入图片描述

其中类似于 KMP, nxt 数组就是字符串 PP 自身匹配的结果,可以写出下面的代码。


inline void getFail(char *P, int *nxt) 
{
    int a = 0, p = 0, m = strlen(P);
    nxt[0] = m;
    for(int i = 1; i < m; i++)
    {
        if(i + nxt[i - a] >= p)
        {
            p = max(p, i);
            while(p < m && P[p] == P[p - i]) p++;
            nxt[i] = p - i; a = i;
        }
        else nxt[i] = nxt[i - a];
    }
}
inline void getExtend(char *T, char *P, int *nxt, int *extend)
{
    int a = 0, p = 0;
    int m = strlen(P), n = strlen(T);
    getFail(T, nxt);
    for(int i = 0; i < n; i++)
    {
        if(i + nxt[i - a] >= p)
        {
            p = max(i, p);
            while(p < n && p - i < m && T[p] == P[p - i]) p++;
            extend[i] = p - i; a = i;
        }
        else extend[i] = nxt[i - a];
    }
}

算法的时间复杂度

KMP 算法的时间复杂度

KMP 算法由初始化和进行匹配两部分进行,对于初始化算法部分,我们来证明每一次的 while 总共会进行 O(P)O(|P|) 次,首先:每一次 nxt[i] 的增长最多是 1,也就是最多增长 P|P| 次,其次:每一次 while 循环只会降低 nxt[i] 的值而不会增加。综合这些因素,递减来源于 while 循环,最多下降 P|P| 次,所以复杂度为 O(P)O(|P|)。这只是一个大致的感性理解,有兴趣的读者可以使用部分摊还分析的知识。并且类比于初始化操作,匹配的复杂度是 O(T)O(|T|)

KMP 算法预处理 O(P)O(|P|),匹配 O(T)O(|T|),是优秀的线性时间算法。

exKMP 算法的时间复杂度分析

下面来分析一下 exKMP 算法的时间复杂度,通过上面的算法介绍可以知道,对于第一种情况,无需做任何匹配即可计算出 extend[i],对于第二、三种情况,都是从未被匹配的位置开始匹配,匹配过的位置不再匹配,也就是说对于母串的每一个位置,都只匹配了一次,所以算法总体时间复杂度是 O(T)O(|T|)的,同时为了计算辅助数组 next[i] 需要先对字串 PP 进行一次 exKMP 算法处理。

KMP 算法预处理 O(P)O(|P|),匹配 O(T)O(|T|),也是优秀的线性时间算法。

后记

我在学习 KMP 算法的过程中,遇到了种种的问题,写这篇博客一是为了记住其中的要点,二是希望可以帮助后面的学习者掌握这样一个算法。对于更深入的讨论,可以访问下面的链接。

exKMP算法的优秀讲解

关于KMP算法时间复杂度的讨论

对于 KMP 算法,有 MP 与 KMP 两种,在 OI 中大多数以 MP 代 KMP,本文中介绍的实际是 MP 算法,因为 MP 算法的 nxt 数组有着更多的性质,而且时间效率已经非常不错。下面提供一篇关于 MP 与 KMP 的对比文章。

KMP与MP相关

感谢大家的阅读。

展开阅读全文

没有更多推荐了,返回首页