KMP

  KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。(摘自百度百科


  KMP算法算是一个实现上不是很难,应用上不是很广,原理上也不是太复杂的一个算法。但是不知道为什么,总是有各种奇奇怪怪的考试也好面试也罢甚至是ACM或者数据结构课都需要你了解这个算法(好吧,其实是因为字符串匹配在计算机中是个很重要而又很基本的工作)。个人认为,算法的精髓不在于你如何实现它,而在于体会它内在的思想。但是到现在为止很少见有教材或博客真真正正地用人话讲解了这个算法,大多都是写写什么前后缀,告诉你它比传统算法好,顺便贴上代码,然后草草了事。这类文章,懂了的人会说:“讲得没毛病”,而不懂的人的反应一般是:“EXM???”……

  所以在这里希望能用稍微浅显易懂的方法来讲解下这个算法的思路(而非实现),当然,建议读者在阅读本文章前先了解最传统的字符串匹配方法的思路(就是一个一个比较的那个)。

  首先来说一下什么叫字符串匹配,所谓字符串匹配呢,就是从一个串里面找到你想要的内容。比如在这篇博客里找到“哦卡卡”这几个字,或者从一个十页的获奖名单中找到你的名字。在这里,被查找的那个串我们叫它主串,你想查找的串叫做模式串。引用前面的栗子的话,获奖名单就是主串,而你的名字就是模式串。

  另,附上前后缀概念:字符串的前缀是指字符串的任意首部。比如字符串“abbc”的前缀有“a”,“ab”,“abb”,“abbc”。同样,字符串的任意尾部是字符串的后缀,“abbc”的后缀有“c”,“bc”,“bbc”,“abbc”。 (摘自ACM水题--字符串的前缀和后缀

  请注意,在后面的匹配内容中,对主串来说,浅蓝色的部分是计算机“完全不知道”的内容,换句话说,是计算机并没有处理到的内容。绿色的是已经处理完的内容,无须再次处理(不用再管了)。黑色的是正在处理的内容,也就是失配的位置(一定程度上来说,你可以把黑色部分看成是未知的)。另,红色的是模式串与主串匹配的部分,即相同的部分。并且我们假设,主串是无限长的,也就是说在“管它后面是什么”之后还有内容,只是没有写出。

  然后我们来说说字符串匹配的这个过程。传统的方法是拿模式串与主串从头到尾依次比较,然而这样的话可能遇到一些坑爹的情况(匹配1):

管它前面是什么弗拉基米尔·弗拉基米罗维奇·普它后面是什么
         弗拉基米尔·弗拉基米罗维奇·普

  在这里,我们发现主串的大部分都和我们的模式串相同,除了最后一个字。如果使用传统的方法,我们下一次匹配的场景是这样的(匹配2):

管它前面是什么弗基米尔· 弗拉基米罗维奇· 普管它后面是什么
          弗拉基米尔· 弗拉基米罗维奇· 普京

  用这种方法完全放弃了之前已经匹配过了的部分,很蠢,对不对?下面,我们希望找到一种方法,能让我们不必像传统方法一样,在失配时只移动一位,而是“跳”得稍微远一点。我们假设我们完全掌握了已匹配内容的全部信息,并且在已匹配部分所做的任何操作的代价都是O(1)。首先可以确定,如果主串的某一位置能让模式串匹配,那么这个位置往后的字符和模式串的某一前缀一定相同。而如果我们找到了这样的位置,我们就可以直接让模式串跳跃到该处。

  下面让我们来观察一下匹配1主串中的已匹配部分(红色),用之前描述的思路,我们找到了如下的两个部分(匹配3):

管它前面是什么弗拉基米尔·弗拉基米罗维奇·普它后面是什么
         弗拉基米尔·弗拉基米罗维奇·普

  所以,如果电脑能聪明一点,它至少会这样移动模式串(匹配4):

管它前面是什么弗拉基米尔·弗拉基米维奇· 普管它后面是什么
               弗拉基米尔· 弗拉基米罗维奇· 普京 

  但是问题在于,如果我们像匹配4一样移动模式串,虽然有一部分是匹配的,但很明显地,模式串与主串在那个位置最后一定是不匹配的,因为模式串中是弗拉基米尔,而主串那个位置是弗拉基米罗(参见匹配3与匹配4)。那么除了匹配4所示的位置,主串已匹配的部分里还有没有满足条件的位置呢?答案是没有。于是乎,我们可以直接这样移动(匹配5):

管它前面是什么弗拉基米尔·弗拉基米罗维奇·普后面是什么
                                  弗拉基米尔·弗拉基米罗维奇·普京

  为什么这样移动呢?因为我们在前面已经确定,不管我把模式串的串头放在主串已匹配部分的什么地方,模式串与主串在那个位置都一定是不匹配的。所以我们可以直接将模式串移动出已匹配区域(即移动到“管”处)。所以如果我们充分利用已知部分,在匹配1后就可以直接跳到匹配5。这就是我们可能遇到的第一种情况:已匹配的主串中,不存在任何可能的位置能够匹配模式串

  (另外,我们能从这个例子中得到一个小结论:相同部分是模式串已匹配部分的前缀时,该部分才有用,但并不是所有的这种部分都有用)

  下面我们修改一下主串,看看其他的情况。如下(匹配6):

管它前面是什么弗拉基米尔·弗拉基米它后面是什么
       弗拉基米尔·弗拉基米罗维奇·普京

  这个例子依旧是匹配到一半发现并不匹配。而在这里,我们在已匹配部分的最后几个字中(我们叫它后缀)发现了可重复利用的空间(匹配7):

管它前面是什么弗拉基米尔·弗拉基米它后面是什么
       弗拉基米尔·弗拉基米罗维奇·普京

  观察匹配7加重部分,我们发现,当相同的部分是主串已匹配部分的后缀时,由于我们并不知道主串已匹配部分后面的内容是什么,所以我们只能通过移动验证,于是,我们可以这样移动字符串(匹配8)

管它前面是什么弗拉基米尔·弗拉基米它后面是什么
             弗拉基米尔·弗拉基米罗维奇·普京

  这里为了不至于混淆,匹配8只进行了移动,没有修改两个串的配色。下面的是修改配色后的版本(匹配9)

管它前面是什么弗拉基米尔·弗拉基米它后面是什么
             弗拉基米尔·弗拉基米罗维奇·普京

  当我们无法用已匹配部分来验证模式串是否能继续匹配时,我们只能通过移动模式串来使其到达那个不知道能不能成功的位置来验证(因为我们使用的是已匹配的部分,即红色部分,所以黑色和蓝色对于我们都是未知的,所以说这种移动的结果是未知的)。这就是我们可能会遇到的第二种情况:已匹配串中存在可能的位置使模式串匹配成功

  (另外,我们获得了之前结论的补充:相同部分是主串的已匹配部分的后缀时,该部分才可能有用)

  经过上面两个小栗子,我们获得了从已匹配部分中得到可用相同部分的条件:

    1.相同部分必须是主串已匹配部分的后缀

    2.相同部分必须是模式串已匹配部分的前缀

  而当不存在可用部分时,我们直接将模式串移出已匹配部分

  这两个条件有什么用呢?很简单,如果你在匹配过程中失配了,那么你就可以从主串已匹配部分的尾巴和模式串已匹配部分的脑袋开始找,看看当主串已匹配部分的后缀和模式串已匹配部分的前缀长度相同时,两个子串是不是相同。如果存在相同的,那么恭喜你,你可以直接把模式串移动到那个位置了!(就像匹配6到匹配9)如果不存在相同的,那么直接将该串移出主串已匹配部分(就像匹配1到匹配5)

  但是,事情真的有那么好么?

  考虑下面的情况(匹配10):

管它前面是什么弗拉基米·弗拉基米·弗拉基米·弗拉基米罗维奇·普京管它后面是什么
       弗拉基米·弗拉基米·弗拉基米罗维奇·普京

  我们发现,在这个栗子里,存在着两种可能的寻找方法(匹配11):

管它前面是什么弗拉基米·弗拉基米·弗拉基米·弗拉基米罗维奇·普京管它后面是什么
       弗拉基米·弗拉基米·弗拉基米罗维奇·普京

  以及(匹配12):

管它前面是什么弗拉基米·弗拉基米·弗拉基米·弗拉基米罗维奇·普京管它后面是什么
       弗拉基米·弗拉基米·弗拉基米罗维奇·普京

  很明显,模式串在主串中是存在的,但,如果我们简单地以前缀与后缀相同作为评判标准而以匹配11的方式移动,我们会得到错误的结果。

  而避免这种问题的方式也很简单:让模式串移动的距离尽可能少,也就是让相同部分尽可能长。于是,我们得到了第三个小结论:当存在多个可用的相同部分时,取最长的进行移动

  

  到现在为止,只要我们得到了已匹配部分,就可以直接运用它实现模式串的“跳跃”了,然而,如果我们每失配一次就算一下需要跳跃的距离……这算法的复杂度和传统方法也就没啥差别了。所以呢,让我们看一下怎样才能让我们的“跳跃”更加快速。

  还是用栗子说话,观察下面两个栗子:

  (匹配13)

管它前面是什么弗拉基米尔·弗拉基米它后面是什么
       弗拉基米尔·弗拉基米罗维奇·普京

  (匹配14)

管它前面是什么弗拉基米尔·弗拉基米记得我说过管它后面是什么
       弗拉基米尔·弗拉基米罗维奇·普京

  这两个栗子使用了相同的模式串,但是他们的主串是不同的。注意到,二者失配的地方对于模式串来说是相同的(都在“罗”这个字),并且,二者需要跳跃的距离也是相同的(因为已匹配的部分是相同的)。

  好了,由此,我们可以得到第四个小结论:对于同一个模式串,在它的某个位置失配时需要跳跃的距离是固定的

  这个结论有什么用呢——如果我计算出了在模式串某个位置失配时所需要跳跃的距离,那么在我下一次在该位置失配时,我可以不必再计算一次。

  以这个结论为基础,我们可以大大节省计算跳跃距离所需要的时间。但是,仅有这些我们还是不会满足的(我就当你不会了),如果能够让我在一开始就得到所有位置的跳跃距离,那么,在匹配的过程中不就可以非常愉快地蹦蹦蹦了么?

  让我们先整理一下之前的结论

    1.在失配时,我们可以通过寻找主串与模式串已匹配部分的相同部分来获得有用信息

    2.相同部分必须是主串已匹配部分的后缀

    3.相同部分必须是模式串已匹配部分的前缀

    4.当存在多个可用的相同部分时,取最长的进行移动,如不存在可用的相同部分,则将模式串移出主串已匹配部分

    5.对于同一个模式串,在它的某个位置失配时需要跳跃的距离是固定的

  现在我们需要做的是寻找一种方法,让我们可以预处理出在模式串某位置失配时需要跳跃的长度。

  跳跃距离是通过比较主串已匹配部分的后缀和模式串已匹配部分的前缀得来的,而问题的关键在于,如果失配的位置确定了,那么在当前匹配中,主串的已匹配部分和模式串的已匹配部分一定是相同的。所以只需要比较模式串的已匹配部分的前后缀就可以得到跳跃的距离。

  举个栗子(匹配14)

管它前面是什么弗拉基米尔·弗拉基米管它后面是什么
       弗拉基米尔·弗拉基米罗维奇·普京

  之前,我们是拿主串已匹配部分的后缀与模式串已匹配部分的前缀作比较,但因为这二者是相同的,所以,我们完全可以用模式串已匹配部分的前缀与模式串已匹配部分的后缀作比较,而得到的结果是完全相同的。

  进一步讲,在拿到模式串以后,我们可以从头至尾假设某一位是失配位,则该位之前一定为已匹配部分,从而可以算出在该位失配时所需要跳跃的距离。

  当我们计算出每一位失配所需的跳跃距离后,在进行匹配时便可以据此进行移动,而不必像传统方法一位一位进行移动。

  

  以上就是KMP算法的基本思想,如有问题欢迎指正。

  KMP的代码就不附了,网上一大堆。

  

作者:Dumblidor

转载请注明出处:http://www.cnblogs.com/Dumblidor/p/6250746.html

2017.1.5

原文地址:https://www.cnblogs.com/Dumblidor/p/6250746.html