通过上一节的介绍,学习了串的普通模式匹配算法,大体思路是:模式串从主串的第一个字符开始匹配,每匹配失败,主串中记录匹配进度的指针 i 都要进行 i-j+1 的回退操作(这个过程称为“指针回溯”),同时模式串向后移动一个字符的位置。一次次的循环,直到匹配成功或者程序结束。

"KMP"算法相比于"BF"算法,优势在于:

  • 在保证指针 i 不回溯的前提下,当匹配失败时,让模式串向右移动最大的距离;
  • 并且可以在O(n+m)的时间数量级上完成对串的模式匹配操作;

故,"KMP"算法称为“快速模式匹配算法”。

模式串向右移动距离的计算

在模式串和主串匹配时,各有一个指针指向当前进行匹配的字符(主串中是指针 i ,模式串中是指针 j ),在保证 i 指针不回溯的前提下,如果想实现功能,就只能让 j 指针回溯。

j 指针回溯的距离,就相当于模式串向右移动的距离。 j 指针回溯的越多,说明模式串向右移动的距离越长。

计算模式串向右移动的距离,就可以转化成:当某字符匹配失败后, j 指针回溯的位置。

对于一个给定的模式串,其中每个字符都有可能会遇到匹配失败,这时对应的 j 指针都需要回溯,具体回溯的位置其实还是由模式串本身来决定的,和主串没有关系。

模式串中的每个字符所对应 j 指针回溯的位置,可以通过算法得出,得到的结果相应地存储在一个数组中(默认数组名为 next )。

计算方法是:对于模式串中的某一字符来说,提取它前面的字符串,分别从字符串的两端查看连续相同的字符串的个数,在其基础上 +1 ,结果就是该字符对应的值。

每个模式串的第一个字符对应的值为 0 ,第二个字符对应的值为 1 。

例如:求模式串 “abcabac” 的 next 。前两个字符对应的 0 和 1 是固定的。

对于字符 ‘c’ 来说,提取字符串 “ab” ,‘a’ 和 ‘b’ 不相等,相同的字符串的个数为 0 ,0 + 1 = 1 ,所以 ‘c’ 对应的 next 值为 1 ;

第四个字符 ‘a’ ,提取 “abc” ,从首先 ‘a’ 和 ‘c’ 就不相等,相同的个数为 0 ,0 + 1 = 1 ,所以,‘a’ 对应的 next 值为 1 ;

第五个字符 ‘b’ ,提取 “abca” ,第一个 ‘a’ 和最后一个 ‘a’ 相同,相同个数为 1 ,1 + 1 = 2 ,所以,‘b’ 对应的 next 值为 2 ;

第六个字符 ‘a’ ,提取 “abcab” ,前两个字符 “ab” 和最后两个 “ab” 相同,相同个数为 2 ,2 + 1 = 3 ,所以,‘a’ 对应的 next 值为 3 ;

最后一个字符 ‘c’ ,提取 “abcaba” ,第一个字符 ‘a’ 和最后一个 ‘a’ 相同,相同个数为 1 ,1 + 1 = 2 ,所以 ‘c’ 对应的 next 值为 2 ;

所以,字符串 “abcabac” 对应的 next 数组中的值为(0,1,1,1,2,3,2)。

上边求值过程中,每次都需要判断字符串头部和尾部相同字符的个数,而在编写算法实现时,对于某个字符来说,可以借用前一个字符的判断结果,计算当前字符对应的 next 值。

具体的算法如下:

模式串T为(下标从1开始):“abcabac”
next数组(下标从1开始):    01

第三个字符 ‘c’ :由于前一个字符 ‘b’ 的 next 值为 1 ,取 T[1] = ‘a’ 和 ‘b’ 相比较,不相等,继续;由于 next[1] = 0,结束。 ‘c’ 对应的 next 值为1;(只要循环到 next[1] = 0 ,该字符的 next 值都为 1 )

模式串T为:                  “abcabac”
next数组(下标从1开始):011

第四个字符 ’a‘ :由于前一个字符 ‘c’ 的 next 值为 1 ,取 T[1] = ‘a’ 和 ‘c’ 相比较,不相等,继续;由于 next[1] = 0 ,结束。‘a’ 对应的 next 值为 1 ;

模式串T为:                  “abcabac”
next数组(下标从1开始):0111

第五个字符 ’b’ :由于前一个字符 ‘a’ 的 next 值为 1 ,取 T[1] = ‘a’ 和 ‘a’ 相比较,相等,结束。 ‘b’ 对应的 next 值为:1(前一个字符 ‘a’ 的 next 值) + 1 = 2 ;

模式串T为:                  “abcabac”
next数组(下标从1开始):01112

第六个字符 ‘a’ :由于前一个字符 ‘b’ 的 next 值为 2,取 T[2] = ‘b’ 和 ‘b’ 相比较,相等,所以结束。‘a’ 对应的 next 值为:2 (前一个字符 ‘b’ 的 next 值) + 1 = 3 ;

模式串T为:                  “abcabac”
next数组(下标从1开始):011123

第七个字符 ‘c’ :由于前一个字符 ‘a’ 的 next 值为 3 ,取 T[3] = ‘c’ 和 ‘a’ 相比较,不相等,继续;由于 next[3] = 1 ,所以取 T[1] = ‘a’ 和 ‘a’ 比较,相等,结束。‘a’ 对应的 next 值为:1 ( next[3] 的值) + 1 = 2 ;

模式串T为:                  “abcabac”
next数组(下标从1开始):0111232

算法实现:

#include <stdio.h>
#include <string.h>
void Next(char *T, int *next)
{
  int i = ;
  next[] = ;
  int j = ;
  while (i<strlen(T))
  {
    if (j==0 || T[i-]==T[j-])
    {
      i++;
      j++;
      next[i] = j;
    }
    else
    {
      j = next[j];
    }
  }
}
注意:在此程序中,next 数组使用的下标初始值为 1 ,next[0] 没有用到(也可以存放 next 数组的长度)。而串的存储是从数组的下标 0 开始的,所以程序中为 T[i-1] 和 T[j-1]。

基于next的KMP算法的实现

先看一下 KMP 算法运行流程(假设主串:ababcabcacbab,模式串:abcac)。

第一次匹配:

匹配失败,i 指针不动,j = 1(字符‘c’的next值);

第二次匹配:

相等,继续,直到:

匹配失败,i 不动,j = 2 ( j 指向的字符 ‘c’ 的 next 值);

第三次匹配:

相等,i 和 j 后移,最终匹配成功。

使用普通算法,需要匹配 6 次;而使用 KMP 算法,则只匹配 3 次。

实现代码:

int KMP(char *S, char *T)
{
  int next[];
  Next(T, next);  //根据模式串T,初始化next数组
  int i = ;
  int j = ;
  while (i<=strlen(S) && j<=strlen(T))
  {
    //j==0:代表模式串的第一个字符就和指针i指向的字符不相等;S[i-1]==T[j-1],如果对应位置字符相等,两种情况下,指向当前测试的两个指针下标i和j都向后移
    if (j== || S[i-]==T[j-])
    {
      i++;
      j++;
    }
    else
    {
      j=next[j];//如果测试的两个字符不相等,i不动,j变为当前测试字符串的next值
    }
  }
  if (j>strlen(T))
  {
    //如果条件为真,说明匹配成功
    return i-(int)strlen(T);
  }
  return -;
}

KMP算法完整代码

#include <stdio.h>
#include <string.h>
void Next(char *T, int *next)
{
  int i = ;
  next[] = ;
  int j = ;
  while (i<strlen(T))
  {
    if (j==0 || T[i-]==T[j-])
    {
      i++;
      j++;
      next[i] = j;
    }
    else
    {
      j = next[j];
    }
  }
}
int KMP(char *S, char *T)
{
  int next[];
  Next(T, next);  //根据模式串T,初始化next数组
  int i = ;
  int j = ;
  while (i<=strlen(S)&&j<=strlen(T))
  {
    //j==0:代表模式串的第一个字符就和当前测试的字符不相等;S[i-1]==T[j-1],如果对应位置字符相等,两种情况下,指向当前测试的两个指针下标i和j都向后移
    if (j== || S[i-]==T[j-])
    {
      i++;
      j++;
    }
    else
    {
      j = next[j];//如果测试的两个字符不相等,i不动,j变为当前测试字符串的next值
    }
  }
  if (j>strlen(T))
  {
    //如果条件为真,说明匹配成功
    return i-(int)strlen(T);
  }
  return -;
}
int main()
{
  int i = KMP("ababcabcacbab", "abcac");
  printf("%d", i);
  return ;
} 运行结果:

升级版的next

注意:KMP 算法的关键在于 next 数组的确定,其实对于上边的KMP算法中的next数组,不是最精简的,还可以简化。

例如:

模式串T:a b c a c
    next  :0 1 1 1 2

在模式串“abcac”中,有两个字符 ‘a’,我们假设第一个为 a1,第二个为 a2。在程序匹配过程中,如果 j 指针指向 a2 时匹配失败,那么此时,主串中的 i 指针不动,j 指针指向 a1 ,很明显,由于 a1==a2,而 a2!=S[i],所以 a1 也肯定不等于 S[i]。

为了避免不必要的判断,需要对 next 数组进行精简,对于“abcac”这个模式串来说,由于 T[4] == T[next[4]] ,所以,可以将next数组改为:

模式串T:a b c a c
    next  :0 1 1 0 2

这样简化,如果匹配过程中由于 a2 匹配失败,那么也不用再判断 a1 是否匹配,因为肯定不可能,所以直接绕过 a1,进行下一步。

实现代码:

void Next(char *T, int *next)
{
  int i = ;
  next[] = ;
  int j = ;
  while (i<strlen(T))
  {
    if (j==0 || T[i-]==T[j-])
    {
      i++;
      j++;
      if (T[i-] != T[j-])
      {
        next[i] = j;
      }
      else
      {
        next[i] = next[j];
      }
    }
    else
    {
      j = next[j];
    }
  }
}

使用精简过后的 next 数组在解决例如模式串为“aaaaaaab”这类的问题上,会减少很多不必要的判断次数,提高了KMP算法的效率。

例如:精简前为 next1,精简后为 next2:

模式串:a a a a a a a b
  next1:0 1 2 3 4 5 6 7
  next2:0 0 0 0 0 0 0 7

总结

KMP 算法,之所以比 BF 算法快的根本原因在于:KMP 算法其实也和 BF 算法一样,都是从主串开头开始匹配,但是在匹配过程中,KMP算法记录了一些必要的信息。根据这些信息,在后续的匹配过程中,跳过了一些无意义的匹配过程。

数据结构20:KMP算法(快速模式匹配算法)详解的更多相关文章

  1. KMP算法 Next数组详解

    题面 题目描述 如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置. 为了减少骗分的情况,接下来还要输出子串的前缀数组next.如果你不知道这是什么意思也不要问,去百 ...

  2. hadoop 0.20.2伪分布式安装详解

    adoop 0.20.2伪分布式安装详解 hadoop有三种运行模式: 伪分布式不需要安装虚拟机,在同一台机器上同时启动5个进程,模拟分布式. 完全分布式至少有3个节点,其中一个做master,运行名 ...

  3. 数据结构图文解析之:队列详解与C++模板实现

    0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...

  4. centos7.2环境elasticsearch-5.0.1+kibana-5.0.1+zookeeper3.4.6+kafka_2.9.2-0.8.2.1部署详解

    centos7.2环境elasticsearch-5.0.1+kibana-5.0.1+zookeeper3.4.6+kafka_2.9.2-0.8.2.1部署详解 环境准备: 操作系统:centos ...

  5. JVM垃圾回收算法及回收器详解

    引言 本文主要讲述JVM中几种常见的垃圾回收算法和相关的垃圾回收器,以及常见的和GC相关的性能调优参数. GC Roots 我们先来了解一下在Java中是如何判断一个对象的生死的,有些语言比如Pyth ...

  6. KMP字符串匹配算法详解

    KMP算法利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的.具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息.时间复杂度O(m+n). Next()函数 ...

  7. 【原创】通俗易懂的讲解KMP算法(字符串匹配算法)及代码实现

    一.本文简介 本文的目的是简单明了的讲解KMP算法的思想及实现过程. 网上的文章的确有些杂乱,有的过浅,有的太深,希望本文对初学者是非常友好的. 其实KMP算法有一些改良版,这些是在理解KMP核心思想 ...

  8. 【数据结构】KMP算法

    我还是不太懂... 转2篇大神的解释    1>https://www.cnblogs.com/yjiyjige/p/3263858.html     2>https://blog.csd ...

  9. 【机器学习】【条件随机场CRF-2】CRF的预测算法之维特比算法(viterbi alg) 详解 + 示例讲解 + Python实现

    1.CRF的预测算法条件随机场的预测算法是给定条件随机场P(Y|X)和输入序列(观测序列)x,求条件概率最大的输出序列(标记序列)y*,即对观测序列进行标注.条件随机场的预测算法是著名的维特比算法(V ...

随机推荐

  1. c语言-树的基础知识

    第一.树的定义:   1.有且只有一个称为根的节点   2.有若干个互不相交的子树,这些子树本身也是一颗树 第二.专业术语: 树的深度:从根节点到最低层,节点的层数 ,称之为树的深度.  根节点是第一 ...

  2. DAY10-MYSQL初识

    一 数据库管理软件的由来 基于我们之前所学,数据要想永久保存,都是保存于文件中,毫无疑问,一个文件仅仅只能存在于某一台机器上. 如果我们暂且忽略直接基于文件来存取数据的效率问题,并且假设程序所有的组件 ...

  3. 浅谈时钟的生成(js手写代码)(非原创)

    在生成时钟的过程中自己想到布置表盘的写法由这么几种: 当然利用那种模式都可以实现,所以我们要用一个最好理解,代码有相对简便的方法实现 1.利用三角函数 用js在三角函数布置表盘的过程中有遇见到这种情况 ...

  4. 第3章 springboot接口返回json 3-1 SpringBoot构造并返回一个json对象

    数据的使用主要还是以JSON为主,我们不会去使用XML. 这个时候我们先不使用@RestController,我们使用之前SpringMVC的那种方式,就是@Controller.  @Respons ...

  5. js 控制标记样式

    做一个变色的标签 鼠标移入变为灰色,移除变回原来的颜色,点击变成黑色,再点击变回,如果变成黑色不受移入移除影响. <body> <div class="bt1" ...

  6. c# json.net xml互转

    json转xml: XmlDocument doc = (XmlDocument)JsonConvert.DeserializeXmlNode(jsonText,"root"); ...

  7. php学习笔记-for循环

    for(init;condition;statement) { func(); } for循环的执行逻辑是先执行一次init语句,然后判断condition是否为true,是则执行func(),再执行 ...

  8. Django框架 之 模板语言

    Django框架 之 模板语言 浏览目录 标签 过滤器 一.标签 Tags 1.普通变量 普通变量用{{ }} 变量名由数字.字母.下划线组成 点.在模板语言中用来获取对象相应的属性值 示例: 1 2 ...

  9. Git 之 与Github交互

    我们不可能只在一台电脑上开发,白天在公司用公司电脑,晚上在家可以用自己电脑.但是这个代码怎么让两台电脑同步呢?总不能用U盘复制粘贴.太繁琐. 这里我们就可以找个代码托管的平台,帮我们做这件事. Git ...

  10. 图--生成树和最小生成树.RP

    树(自由树).无序树和有根树    自由树就是一个无回路的连通图(没有确定根)(在自由树中选定一顶点做根,则成为一棵通常的树).    从根开始,为每个顶点(在树中通常称作结点)的孩子规定从左到右的次 ...