zoukankan      html  css  js  c++  java
  • 理解KMP算法

    问题背景

    在我们平时使用计算机的过程中,有一项使用非常频繁的功能就是字符串查找。这个问题可以描述成:给定一个字符串文本T,要从中找出是否含有某个子串P。我们把P叫做模式字符串。这个问题最直接的解法就是逐个匹配:先将T和P左对齐,从头开始依次比较P中的每个字符是否和T中对应的字符相同。例如,T为“a a b a c a b a b c a b a c a b”,P为“a b a b c a b”,一开始将T和P左对齐并依次比较各个字符是否相同,

    (1) T: a a b a c a b a b c a b a c a b
        P: a b a b c a b

    当我们比较到第二个字符的时候,发现不一样,于是将P右移一位,再从头开始匹配:

    (2) T: a a b a c a b a b c a b a c a b
        P:   a b a b c a b

    这时候我们发现前三个字符是相同的,匹配到第四个字符的时候不同,匹配失败。于是继续将P右移一位,再次进行以上的匹配过程,直至P的所有字符和T中的某个子串匹配。对于上面这个例子,当我们进行到如下所示的比较时,即匹配成功:

    (3) T: a a b a c a b a b c a b a c a b
        P:           a b a b c a b

    如果我们不是这么走运,当比较到T的最后一个字符时,仍然没有完全匹配,则匹配失败。

    我们看到,这种方法虽然可以解决字符串匹配问题,但是效率很低下。


    如何更快地匹配

    在上述的匹配过程中,我们每次只将P右移一位,因此匹配过程很缓慢。通过观察我们发现,有时候仅仅将P右移一位的匹配是明显无效的,从而可以一次右移多位,提高匹配效率。具体来说,我们看上面的第二次匹配,如果仅仅将P右移一位,因为P的第一个字符a和T中已匹配子串(a b a)的第二个字符,也就是P的第二个字符b不同,所以右移一位是明显无效的。我们可以直接将P右移两位进行匹配,这样提高了效率。为什么只右移两位、而不多移几位呢?因为当右移两位的时候,P的第一个字符和T中已匹配子串的最后一个字符相同,这时候我们需要继续比较后面的字符才能确定是否匹配成功。

    更一般化的描述是,假设当前P的前q个字符已经匹配,而第q + 1个字符不匹配,这时候我们的目标是确定最多能将P右移多少位、从而跳过那些明显无效的匹配。从上面的例子可以发现,要完成这个目标,我们只需要知道P的前q个字符的信息即可。这种利用模式字符串本身的局部特征提高匹配效率的方法,就是KMP算法的精髓所在。KMP算法的命名来自它的三个发现者D.E.Knuth,J.H.Morris和V.R.Pratt的名字的首字母,这种方法可以大大提高字符串匹配的效率,从而将求解这一问题降至线性复杂度。

    回到前面的问题,如何利用模式字符串本身的局部特征呢?回顾前面的过程,当我们每次决定能右移多少位的时候,实际是在拿已匹配部分前缀和它本身的后缀相比,我们最多只能右移到已匹配子串的前缀和后缀刚好匹配的位置。前缀和后缀相匹配的部分越长,我们能右移的步数就越少;前缀和后缀相匹配的部分越短,我们能右移的步数就越多。例如,假设当前已匹配部分是a b c a b,它的前缀有四个a、a b、a b c、a b c a,对应的后缀有四个b、a b、c a b、b c a b。我们依次比较每一对前缀后缀,可能有多个相匹配的前缀后缀,我们只需要找到最长的那个就OK啦,如下:

    a b c a b     | a b c a b        | a b c a b
      a b c a b   |     a b c a b    |       a b c a b

    当执行第三次比较时,前缀后缀匹配(都是a b),最大匹配长度是2。已匹配子串总长度是5,所以这时可以直接将P右移5 - 2 = 3位(因为前两次右移已经明显无效啦)。

    可以设想,对于q = 1, 2, ... , m,其中m为模式字符串P的长度,如果我们已经找到P的每个子串P[1...q]的最大前缀后缀匹配长度,记为π[q],那么当P的第q+1个字符和T中对应字符不匹配时(意味着前q个字符已匹配),我们可以直接将P右移q-π[q]位、从而跳过那些明显无效的匹配。这里的π[q]称为前缀函数他的元素定义为:

    定义一:π[q]的值为P的子串P[1...q]的最大前缀后缀匹配长度。

    有了π[q],我们就可以执行KMP快速匹配了。


    如何计算前缀函数

    在实际操作中,给定模式字符串P,我们先要求出它的前缀函数π[q],然后执行快速匹配操作。本文到此为止,都很好理解。而如何计算前缀函数,才是比较难理解的部分,《算法导论》上关于这一部分,又是符号又是公式,又是定理又是引理的,看的让人云里雾里(可能是为了完整的描述和证明这个问题)。当然,你依然可以用蛮力的方法计算前缀函数,这里介绍一种更高效的方法。

    因为显然π[1] = 0,所以考虑用递归方法。假设我们已经知道k = π[q](设k > 0(条件1))的值,要求π[q+1],即子串P[1...(q+1)]的最大前缀后缀匹配长度。根据定义,P[1...q]的最大前缀后缀匹配长度为π[q],如下图,用红色x表示,这时再增加一个字符P[q+1]时,前缀后缀是否依然匹配呢?我们先比较P[q+1]与P的第π[q] + 1 = k + 1个字符,即两个*表示的字符:

    Index : 1 2 ... π[q] π[q]+1 ... @ @ ... q q+1
    P : x x ... x * ... x x ... x *

    (注:上图中两个@符号表示的下标分别为q - π[q] + 1和q - π[q] + 2。)

    如果P[q+1] == P[k+1](条件2),那么可以直接增加第q+1个字符、且此时前缀后缀刚好匹配,所以π[q+1] = π[q] + 1 = k + 1。然而,大多数时候我们没有这么“走运”,即P[q+1] != P[k+1](条件3),然而这时我们不能直接得到π[q+1] = 0。因为虽然P[q+1] != P[k+1],但是如果前缀子串P[1...π[q]](也是子串P[1...q]的后缀,上图中的红色部分)本身又有相匹配的前缀后缀,假设长度为l,0 < l < π[q],也即P[1...q]的前l个字符和它的后l个字符匹配(令k = l,这时又回到了条件1),那么我们还要将字符P[q+1]和P[l+1]比较,如果P[q+1] == P[l+1]回到条件2),那么π[q+1] = l + 1;否则回到条件3)我们要继续查看子串P[1...k]中是否有相匹配的前缀后缀……如此往复,直至匹配子串中找不到相匹配的前缀后缀(条件4)--就拿上图来说,假设子串P[1...π[q]]没有相匹配的前缀后缀,此时,我们直接比较P[q+1]和P[1],如果P[q+1] == P[1],则π[q+1] = 1;否则π[q+1] = 0。这样,就可以求出π[q+1]的值;利用递归就可以求出π的所有元素。

    那么问题来了,子串P[1...π[q]]本身的最大前缀后缀匹配长度是多少?咦?这句话是不是很熟悉?没错,这就是我们前面给出的定义一,答案是π[π[q]]。这里递归地利用了前面的求解结果,这也是整个问题最难理解的部分。如果这里理解了,剩下的问题就简单了。

    理解上述过程的核心点是:当比较到第q+1个字符不匹配时,我们无论将P右移多少位进行后续比较,必然要先比较不匹配位置之前的子串是否匹配。只有 那些右移后子串仍然匹配的移动才可能是有效的。如下图,假设一开始已匹配子串为P的前5个字符,而第6个字符不匹配(一个是&,一个是#)。这时,无论我们将P右移多少位,假设右移3位,必然要先比较&前面的两个字符(即已匹配子串的最后两个字符)和P的前两个字符(也是已匹配子串的前两个字符)(图中红色标出的x)是否匹配,只有这两个字符先匹配了,我们才需要再比较&和P的第三个字符是否相同。

    T:        ................x x x x x & .......................
    P:                        x x x x x # ...
    P右移3位: x x x x x # ..

    这里我们给个例子说明以上求解过程。假设 P = “a b a b a a c”,要求π[q]。显然π[1] = 0。

    π[2],q = 1,因为π[1] = 0,满足(条件4),所以比较P[2]和P[1],不相等,所以π[2] = 0。

    π[3],q = 2,满足(条件4),所以比较P[3]和P[1],相等,所以π[3] = 1。

    π[4],q = 3,满足(条件1),所以比较P[4]和P[π[3]+1] = P[2],相等,满足(条件2),所以π[4] = π[3] + 1 = 2。

    π[5],q = 4,满足(条件1)所以比较P[5]和P[π[4]+1] = P[3],相等,满足(条件2),所以π[5] = π[4] + 1 = 3。

    π[6],q = 5,满足(条件1)所以比较P[6]和P[π[5]+1] = P[4],如下图

    Index: 1 2 3 4 5 | 6 7
    P : a b a
    b a | a c
    P : a b a b a | a c

    图中红色部分是子串P[1...5]相匹配的最大前缀后缀。虽然P[6] != P[4]满足(条件3),但是我们不能直接得到π[6] = 0;这时还要看P[1...π[5]]是否有相匹配的前缀后缀及其最大长度是多少,答案是l = π[π[5]] = π[3] = 1 > 0,回到(条件1)所以继续比较P[6] 和 P[l + 1] = P[2],不相等,满足(条件3),因此我们继续查看P[1...l] = P[1...1]是否有相匹配的前缀后缀及其最大长度是多少,答案是l = π[1] = 0,满足(条件4),因此比较P[6]和P[1],相等,所以 π[6] = 1。

    π[7],q = 6,满足(条件1)所以比较P[7]和P[π[6]+1] = P[2],不相等,满足(条件3),因为l = π[π[6]] = π[1] = 0,满足(条件4),因此比较P[7]和P[1],不相等,所以 π[7] = 0。

    例子完。


    计算前缀函数的伪代码

     最后给出计算前缀函数的伪代码,转自《算法导论》:

    COMPUTE-PREFIX-FUNCTION(P)
    1  m = length(P)
    2  π[1] = 0
    3  k = 0
    4  for q = 2 to m
    5    while k > 0 and P[k + 1] != P[q]
    6      k = π[k]
    7    if  P[k + 1] == P[q]
    8      k = k + 1
    9    π[q] = k
    10  return π

    首先可以保证第5行的k一定等π[q - 1]:当q = 2时,第一次进入第5行,第2行和第3行初始化保证了k = π[2 - 1] = π[1] = 0;其它情况下代码第9行对此做了保证。所以在计算π[q]时,我们首先判断是否满足条件1(第5行:k > 0),同时判断是否满足条件3(第5行:P[k + 1] != P[q]),如果都满足,则更新k的值(第6行)并继续判断;否则,如果满足条件2(第7行:P[k + 1] == P[q]),则将k值加1(第8行);如果不满足条件2,即满足条件4,此时k = 0,所以直接将新的k值赋给π[q](第9行)。最后,对所有的q都计算完成后,返回前缀函数(第10行)。

     

  • 相关阅读:
    javascript优化--10模式(设计模式)01
    javascript优化--09模式(代码复用)02
    限制字符个数显示
    title
    点选词高亮算法
    ng-repeat嵌套的$index
    console.log的问题
    outsideClick 功能,探索
    input 框的 placeholder 另类实现
    ie9 与 placeholder 问题
  • 原文地址:https://www.cnblogs.com/kane1990/p/4805744.html
Copyright © 2011-2022 走看看