KMP算法详解

来源:互联网 发布:linux . 编辑:程序博客网 时间:2024/06/06 18:36

KMP算法简介

本文主要对KMP的实现方式进行简单的介绍,主要内容如下所示:

  • 什么是KMP
  • KMP的实现思路
  • 算法的代码实现方式
  • 代码完善

什么是KMP

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特-莫里斯-普拉特操作(简称KMP算法) —— [ 百度百科 ]

之前上学做JavaWeb开发的时候,为了偷懒从网上第一次看到了KMP算法,当时花了好长时间才搞懂。KMP有一个很有趣的名字“看毛片”算法,主要用来高效率的进行字段匹配。虽然算法提出时间比较早而且网上资料众多,但是由于算法本身比较晦涩 所以鲜有博主能简洁明了的将其讲清楚,本文主要集中讲解KMP跳表部分。

KMP的实现思路

      上文说到KMP的主要作用就是字段匹配,即给定两个String类型的参数O和f,长度分别为n和m(m<=n)判断B是否出现在O中,如果出现了就返回出现位置的index。

      常见的思路是从O的第一个字节开始依次遍历和f进行比较,如果发现相同的就返回index,但是这种方法的复杂度为O(nm),KMP通过一个复杂度为O(m)的跳表将复杂度降低到了O(n+m)。
      假设我们在O中找f,两个String的1到i-1位是想等的,第i位不相等,通常的做法是将f前移1位,但是kmp的做法是将f前移多位,而具体移动多少位 就是通过跳表计算出来的。
      假设我们知道前移K(k<=i)位,反向分析 我们可以得出结论:
        -A是黑色部分的头部
        -B是黑色部分的尾部
        -A和B是相等的两个String

      高宏宇的CSDN

      通过上面的讲解我们可以发现KMP的跳表就是一个计算图中A和B最大长度的工具,当然这里的长度不可以是f本身,因为这样的话最大长度就只能是f自己了,下面我们来看看跳表的具体生成方式。我们的例子是从BBC ABCDAB ABCDABCDABDE中寻找ABCDABD
1.第一步是把B和A进行比较,因为不一样 所以后移一位
高宏宇的CSDN

2.然后B和A进行比较,因为不一样 所以后移一位
高宏宇的CSDN

3.因为A和A一样,即字符串中出现了和分词索引一样的字节,所以本次不移动
高宏宇的CSDN

4.然后比较下一个,A===A 还是不移动
高宏宇的CSDN

5.直到D和O中的空格不一样了,我们停下来
高宏宇的CSDN

6.现在很多同学的反应时f向后移动一位,再次进行比较,这样做虽然可以 但是性能不高因为第A-D之前的位置我们是比较过的,没必要再重复比较一次
高宏宇的CSDN

7.我们观察得到,空格和D不匹配的时候前6个字符是相同的,所以我们就利用这个线索跳过已经比较过的位置,将f向后移动多位以提高效率
高宏宇的CSDN

8.我们先给出一张跳表,至于这个是如何产生的 详见文章末尾

被搜索词 A B C D A B D 部分匹配值 0 0 0 0 1 2 0

9.我们现在有了跳表,知道前6个字符是相同的,KMP给出了如下计算公式:下次后移位数=已经匹配的字符数-跳表中对应的数字,我们这里对应的就是6-2=4,所以我们可以后移4位。
高宏宇的CSDN

10.因为C和空格不一样,这是前面相同的字符是2,对应跳表的值是0,所以2-0=2,后移2位
高宏宇的CSDN

11.因为A好空格不匹配,后移一位
高宏宇的CSDN

12.后面进行多次比较,我们发现C和D不一样,意识6-2=4,我们后移4位
高宏宇的CSDN

13.按照前面你的规则进行比较,知道f的最后一位完全匹配,7-0=7,我们移动7位,到目前为止我们在A中发现了第一个完全匹配的f
高宏宇的CSDN

      下面我们介绍跳表的生成,在这之前我们来介绍两个概念,前缀还有后缀,“缀”的含义就是除了字符串本身以外的所有连续的字符串组合,前缀就是从前开始 后缀就是从后开始。下面我们以一个例子来作为讲解。
        -字符串 abcd
        -前缀 a,ab,adc
        -后缀 d,cd,bcd
      有了这两个概念,我们来介绍跳表是如何产生的。以例子中的ABCDABD为例。

搜索词 匹配值 A 前缀和后缀都为空集,共有元素0个 AB 前缀[A],后缀[B],共有元素0个 ABC 前缀[A, AB],后缀[BC, C],共有元素0个 ABCD 前缀[A, AB, ABC],后缀[BCD, CD, D],共有元素0个 ABCDA 前缀[A, AB, ABC, ABCD],后缀[BCDA, CDA, DA, A],共有元素A,长度为1 ABCDAB 前缀[A, AB, ABC, ABCD, ABCDA],后缀[BCDAB, CDAB, DAB, AB, B],共有元素为AB,长度为2 ABCDABD 前缀[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀[BCDABD, CDABD, DABD, ABD, BD, D],共有元素0个

所以就有个如下表格

被搜索词 A B C D A B D 部分匹配值 0 0 0 0 1 2 0

仔细观察上表,我有个大胆的猜想,跳表的作用就是当字符串的首部和尾部有重复的时候,比如ABCDAB中,我们可以直接将首部AB移动到尾部AB的位置,加快比较的速度。

算法的代码实现方式

      通过上面的讲解,我们大致了解了KMP的实现原理,这里我们回归主题看看如何用代码来实现这个算法,首先我们看看跳表的实现。

makeNext = (const char P[],int next[]) => {    let q,k;                             //q:模版字符串下标;k:最大前后缀长度    let m = strlen(P);                   //模版字符串长度    next[0] = 0;                         //模版字符串的第一个字符的最大前后缀长度为0    for (q = 1,k = 0; q < m; ++q)        //for循环,从第二个字符开始,依次计算每一个字符对应的next值    {        while(k > 0 && P[q] != P[k])     //递归出最大的相同的前后缀长度k            k = next[k-1];        if (P[q] == P[k])                //如果相等,那么最大相同前后缀长度加1        {            k++;        }        next[q] = k;    }}

代码完善

      待续 还在构思中…