KMP算法是怎么被设计出来的

2023-11-17

定义

我们假设要在主串中寻找子串出现的所有位置
我们记主串中的开始位置为匹配位置,如在 “abc” 中匹配 “bc” ,则匹配位置为 (2)

暴力

我们把匹配过程拆解为

  1. 枚举匹配位置
  2. 验证主串从匹配位置开始是否一一匹配子串

以此,有显然的 O ( n m ) O(nm) O(nm) 算法

基于优化推出KMP

主串:ABCABCABD
子串:ABCABD

  1. 枚举匹配位置 (1),已经验证了ABCAB是匹配的,现在发现C和D匹配不上
  2. 显然我们该要枚举下一个匹配位置,但是我们其实已经在刚刚的验证过程中知道了下一个匹配位置 (2) 的前几位就是 BCAB,同理下下位 (3) 就是 CAB,这些一定会和已经看到了的 ABCAB 就匹配失败的,我们有没有办法直接让他跳到最近的下一个可以匹配得上的位置,也就是 (4) AB 呢。
  3. 我们考虑思考匹配位置从 (i) 变到 (i+1) 意味着什么
    对于主串来说:匹配位置右移,我们将看到的主串就等于已经匹配的 ABCAB 靠左切掉一个字母 (ABCAB → \rightarrow A|BCAB)
    对于子串来说,匹配位置右移,相当于已匹配长度少了 1,也就是 ABCAB 靠右切掉一个字母 (ABCAB → \rightarrow ABCA|B)
    此时匹配位置向右挪 x 位能否匹配就相当于问,已看到的 ABCAB 左边去掉 x 位是否和右边去掉 x 位相同,因为我们要按顺序找到下一个最近的匹配位置,所以最小的 x (x>0) 等于在问,最长前后公共缀,然后总长度减它就是 x
    前后公共缀:同时是串的前缀和后缀,如ABACABA的前后公共缀有A,ABA,(ABACABA本身也是,但是x不为0,相当于一定要删点东西,所以我们不考虑串本身)
  4. 我们考虑把枚举下一个匹配位置想象成主串不动,子串和它的头向后拖,然后 nxt[i] 直接记作 i 失配要跳到哪个位置比如
    主串 ABCABCABD
    子串 ABCABD,此时 i=6 , j=6 失配,因为nxt[6]=3
    -> 跳到 i=6 j=3
    主串 ABCABCABD
    子串         ABCABD
  5. 显然存在一种情况就是没有不是本身的前后公共缀,如 ABC
    主串 ABCABCD
    子串 ABCD       此时 i=4 , j=4 失配
    就说明匹配位置 (1) (2) (3) 都是不可能的
    那么下一个匹配位置就是 nxt[4]=1
    主串 ABCABCD
    子串         ABCD 此时i=4 , j=1
  6. 如果匹配成功,到达子串最后一位了,我们假假地认为子串结尾有一个唯一存在的字符,那么再匹配一位就相当于又失配了,所以我们可以定义出 nxt[len+1],它取决于整个子串的最长公共前后缀

求 nxt 数组

求 nxt 数组的过程可以先想象成对子串本身的一次 kmp

  1. 先以子串 ABACABA 为例
    假设要求nxt[7],已经匹配出nxt[6]的最长公共缀是ABACABA,那么加入最后一位 A 之后,发现这时候能接着匹配上,那么得到 nxt[7]=nxt[6]+1
  2. 如果匹配不上,如 s = “ABACABAB” ,要求nxt[8],已经匹配出nxt[7] ABACABAB,加入的 B 和前面的 C 失配,那么问题就变成了求前面的 ABA 靠右切 x-1,后面的 ABAB 靠左切 x,相等的最小 x,所以我们又变成了求 ABAB 的最长公共缀,这里 ABA 是 nxt[7] 已经匹配的,然后再加上第 8 位 B,而这又相当于把s[4]替换成B,所以我们变成了求 s[4]=B 时的 nxt[4],不难发现继续下去我们得到了一个递归结构,边界就是跳到头,那么就说明不存在

代码实践

相同的思想,不同的实现之间有比较大的差别,以下给出最简练的代码和解释

示例代码的下标从0开始记
不难发现nxt天然即和下标有关,又和公共缀长度有关,是相等还是差1取决于下标起点

/*求nxt,s2是子串,m是子串长度*/
nxt[0]=nxt[1]=0;
for(int i=1;i<=m;i++){
	int j=nxt[i];
	while(j && s2[j]!=s2[i]) j=nxt[j];
	//s2前后相同i+1就记j+1了,不同则j=nxt[j],递归成子问题
	nxt[i+1]=s2[j]==s2[i]?j+1:0;
	//nxt=0表示不存在公共缀,从头开始
}

/*求匹配位置并输出*/
for(int i=0,j=0;i<n;i++){
	while(j && s2[j]!=s1[i]) j=nxt[j];
	if(s2[j]==s1[i]) j++;
	if(j==m){
		printf("匹配位置:%d\n",i-(m-1));
		j=nxt[j];
	}
}

时间复杂度


图片来自"云端潜行" 遵循 CC 4.0 BY-SA 版权协议
这张图展示了KMP的匹配过程,从代码上看,最令人担心的就是每个 for 内部的 while,我们极端化,while 每次都 while 到头,情况如上图,那么我们发现每次 while 中 j j j 回退的次数最多是这个周期中 i i i 增加的个数,所以 j j j 改变量的和不超过 i i i 改变量的和,所以加起来是小于 2 N 2N 2N
nxt 数组和 KMP 匹配的过程类似

总结

kmp 的后半部分就是在失配后,充分利用前面已经验证出来的匹配部分来减少试错
kmp 的前半部分求 nxt 也差不多

改进

nxt 的设计上还有一种改进,假设子串 abaabaaba
nxt[6] 正常是 3,但是我们可以发现s[3]=s[6],所以 6 失配,3 一定也会失配的,所以我们直接把nxt[6] 指到 nxt[3]
同理我们发现 s[9]=s[6] , 所以 nxt[9] 本来是指到 6 的,现在指到 nxt[6] ,因为 nxt[6] 又指到了 nxt[3],所以结果是 nxt[9]=nxt[3]
这种改进是基于我们在观察失配时只考虑了失配前的已匹配串,但我们其实可以把失配的那个位置的字母也考虑进去。如果我们不处理这一优化,kmp 进行时会自己一路跳下去,如果这种情况出现多次,那么我们提前处理就会加速。
经测试,对于随机数据,可以加速 5-10%

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

KMP算法是怎么被设计出来的 的相关文章

随机推荐

  • arouter 自定生成autowired

    原文地址 Evil Mouth s Blog ARouter Autowired 自动注入 May 31 2020 前言 ARouter 有一个 Autowired 的注解 能自动帮我们赋值一些变量 例如 public class Main
  • HBase 维护--查看HLog和HFile

    查看HLog 看了一些文章 HBase高可靠性是实现了HLog Write ahead Log 机制 那么HLog到底存在哪里了呢 首先去HDFS的 hbase目录查看一下 hadoop fs ls R hbase 可以看到hbase下面有
  • MariaDB数据库服务器

    目录 一 什么是数据库 二 什么是关系型数据库 三 数据库字符集和排序规则是什么 四 常用数据类型 五 Mariadb数据库相关配置案例 一 什么是数据库 数据库 DB 是以一定方式长期存储在计算机硬盘内 能与多个用户共享 具有尽可能小的冗
  • android 自动获取短信,安卓app怎样获取短信验证码自动输入

    这个你要自己写吗 我建议你直接调用短信平台的接口不就可以了吗 短信发送 接口地址 String url http 183 203 28 5 9000 HttpSmsMt 下发时间 String mttime new SimpleDateFo
  • [翻译] ProtoBuf 官方文档(全)

    ProtoBuf CSDN搜索 https so csdn net so search q ProtoBuf t blog u chuifuhuo6864
  • nginx重启命令

    nginx s reload 修改配置后重新加载生效 nginx s reopen 重新打开日志文件 nginx t c path to nginx conf 测试nginx配置文件是否正确 关闭nginx nginx s stop 快速停
  • 解决在Anaconda下安装tensorflow报错的问题 ModuleNotFoundError: No module named ‘tensorflow‘

    解决在Anaconda下安装tensorflow报错的问题 Traceback most recent call last File line 1 in ModuleNotFoundError No module named tensorf
  • 宽字节注入入门详解

    原理 GBK 占用两字节 ASCII占用一字节 PHP中编码为GBK 函数执行添加的是ASCII编码 添加的符号为 MYSQL默认字符集是GBK等宽字节字符集 大家都知道 df 被PHP转义 开启GPC 用addslashes函数 或者ic
  • 第二章-注入漏洞

    第二章 注入漏洞 第一节 SQL注入原理 1 1 SQL注入的原因 语言分类 解释型语言和编译型语言 解释型语言是一种在运行时由一个运行时组件解释语言代码并执行其中包含的指令的语言 而编译型语言是代码在生成时转换为机器指令 然后在运行时直接
  • uniapp弹幕滚动到底部

    发布的弹幕至于最底部
  • 【linux】linux 离线安装 curl命令

    文章目录 1 概述 2 curl安装步骤 3 验证 原创不易 且行且珍惜 1 概述 最近在忙一个艰苦的环境 没有yarn界面 没有flink界面 没有es界面 没有kibana界面 条件艰苦 且行且艰险 这个环境发现es日志不入库 然后查看
  • 内网渗透工具-反向代理FRP

    内网渗透工具 反向代理FRP 0x1 简介 FRP是一个比较流行而且成熟的内网渗透工具 支持 TCP UDP HTTP HTTPS 等多种协议 0x2 前期准备 工具准备 可在官方github仓库下载 https github com fa
  • ‘mvn‘不是内部或外部命令

    解决方案有两种 一 1 如果没有安装maven 在IDEA中使用maven 提示mvn不是内部命令 需要在环境变量中的用户变量的Path中添加maven的bin路径 重启下IDEA即可 1 环境变量 用户 2 Path 添加IDEA下的ma
  • Pytorch框架下训练网络的代码结构

    PyTorch 是一个基于 Torch 的 Python 开源机器学习库 用于自然语言处理等应用程序 它主要由 Facebook 的人工智能研究小组开发 PyTorch 提供两个高级功能 1 具有强大的 GPU 加速的张量计算 如 NumP
  • TCP/IP网络编程(6)

    1 IO复用 并发服务器的实现方法 在网络程序中 数据通信时间比CPU运算时间占比更大 因此 采用并发的形式向多个客户端提供服务是一种有效利用CPU的方式 并发服务器的主要实现模型及方法如下所示 多进程服务器 通过常见多个进程提供服务 多路
  • 内存泄漏3____内存泄漏, 内存溢出的区别与关系__内存抖动

    泄漏 memory leak 是指程序在申请内存后 无法释放已申请的内存空间 一次内存泄露危害可以忽略 但内存泄漏堆积后 会变得很严重 无论有多少空间 迟早会被占光 memory leak 最终会导致 OOM out of memory 看
  • web前端三大核心技术

    web前端三大核心技术 根据 W3C 标准 一个网页主要由三部分组成 结构 表现和行为 结构 超文本标记语言 HTML Hyper Text Markup Language HTML用于描述页面的结构 html5 是一门标记型语言 主要由一
  • 列存数据仓库怎样更高效

    很多数据仓库产品都采用了列式存储 如果数据表的总列数很多而计算涉及的列很少 采用列存就只读取需要的列即可 能够减少硬盘访问量 提高性能 特别是数据量非常大时 硬盘扫描和读取的时间占比很大 这时候列存的优势会很明显 那么 是不是只要用了列存就
  • 单链表的建立(C语言):头插法和尾插法建立单链表

    采用头插法建立单链表 该方法从一个空表开始 生成新结点 并将读取到的数据存放到新结点的数据域中 然后将新结点插入到当前链表的表头 即头结点之后 如图2 4所示 图2 4 头插法建立单链表 头插法建立单链表的算法如下 LinkList Cre
  • KMP算法是怎么被设计出来的

    定义 我们假设要在主串中寻找子串出现的所有位置 我们记主串中的开始位置为匹配位置 如在 abc 中匹配 bc 则匹配位置为 2 暴力 我们把匹配过程拆解为 枚举匹配位置 验证主串从匹配位置开始是否一一匹配子串 以此 有显然的 O n m