KMP 算法介绍

1. KMP 算法介绍

KMP 算法:全称叫做 「Knuth Morris Pratt 算法」,是由它的三位发明者 Donald Knuth、James H. Morris、 Vaughan Pratt 的名字来命名的。KMP 算法是他们三人在 1977 年联合发表的。

  • KMP 算法思想:对于给定文本串 T 与模式串 p,当发现文本串 T 的某个字符与模式串 p 不匹配的时候,可以利用匹配失败后的信息,尽量减少模式串与文本串的匹配次数,避免文本串位置的回退,以达到快速匹配的目的。

1.1 朴素匹配算法的缺陷

在朴素匹配算法的匹配过程中,我们分别用指针 i 和指针 j 指示文本串 T 和模式串 p 中当前正在对比的字符。当发现文本串 T 的某个字符与模式串 p 不匹配的时候,j 回退到开始位置,i 回退到之前匹配开始位置的下一个位置上,然后开启新一轮的匹配,如图所示。

这样,在 Brute Force 算法中,如果从文本串 T[i] 开始的这一趟字符串比较失败了,算法会直接开始尝试从 T[i + 1] 开始比较。如果 i 已经比较到了后边位置,则该操作相当于将指针 i 进行了回退操作。

那么有没有哪种算法,可以让 i 不发生回退,一直向右移动呢?

1.2 KMP 算法的改进

如果我们可以通过每一次的失配而得到一些「信息」,并且这些「信息」可以帮助我们跳过那些不可能匹配成功的位置,那么我们就能大大减少模式串与文本串的匹配次数,从而达到快速匹配的目的。

每一次失配所告诉我们的信息是:主串的某一个子串等于模式串的某一个前缀

这个信息的意思是:如果文本串 T[i: i + m] 与模式串 p 的失配是下标位置 j 上发生的,那么文本串 T 从下标位置 i 开始连续的 j - 1 个字符,一定与模式串 p 的前 j - 1 个字符一模一样,即:T[i: i + j] == p[0: j]

但是知道这个信息有什么用呢?

以刚才图中的例子来说,文本串的子串 T[i: i + m] 与模式串 p 的失配是在第 5 个位置发生的,那么:

  • 文本串 T 从下标位置 i 开始连续的 5 个字符,一定与模式串 p 的前 5 个字符一模一样,即:"ABCAB" == "ABCAB"
  • 而模式串的前 5 个字符中,前 2 位前缀和后 2 位后缀又是相同的,即 "AB" == "AB"

所以根据上面的信息,我们可以推出:文本串子串的后 2 位后缀和模式串子串的前 2 位是相同的,即 T[i + 3: i + 5] == p[0: 2],而这部分(即下图中的蓝色部分)是之前已经比较过的,不需要再比较了,可以直接跳过。

那么我们就可以将文本串中的 T[i + 5] 对准模式串中的 p[2],继续进行对比。这样 i 就不再需要回退了,可以一直向右移动匹配下去。在这个过程中,我们只需要将模式串 j 进行回退操作即可。

KMP 算法就是使用了这样的思路,对模式串 p 进行了预处理,计算出一个 「部分匹配表」,用一个数组 next 来记录。然后在每次失配发生时,不回退文本串的指针 i,而是根据「部分匹配表」中模式串失配位置 j 的前一个位置的值,即 next[j - 1] 的值来决定模式串可以向右移动的位数。

比如上述示例中模式串 p 是在 j = 5 的位置上发生失配的,则说明文本串的子串 T[i: i + 5] 和模式串 p[0: 5] 的字符是一致的,即 "ABCAB" == "ABCAB"。而根据「部分匹配表」中 next[4] == 2,所以不用回退 i,而是将 j 移动到下标为 2 的位置,让 T[i + 5] 直接对准 p[2],然后继续进行比对。

1.3 next 数组

上文提到的「部分匹配表」,也叫做「前缀表」,在 KMP 算法中使用 next 数组存储。next[j] 表示的含义是:记录下标 j 之前(包括 j)的模式串 p 中,最长相等前后缀的长度。

简单而言,就是求:模式串 p 的子串 p[0: j + 1] 中,使得「前 k 个字符」恰好等于「后 k 个字符」的「最长的 k。当然子串 p[0: j + 1] 本身不参与比较。

举个例子来说明一下,以 p = "ABCABCD" 为例。

  • next[0] = 0,因为 "A" 中无有相同前缀后缀,最大长度为 0
  • next[1] = 0,因为 "AB" 中无相同前缀后缀,最大长度为 0
  • next[2] = 0,因为 "ABC" 中无相同前缀后缀,最大长度为 0
  • next[3] = 1,因为 "ABCA" 中有相同的前缀后缀 "a",最大长度为 1
  • next[4] = 2,因为 "ABCAB" 中有相同的前缀后缀 "AB",最大长度为 2
  • next[5] = 3,因为 "ABCABC" 中有相同的前缀后缀 "ABC",最大长度为 3
  • next[6] = 0,因为 "ABCABCD" 中无相同前缀后缀,最大长度为 0

同理也可以计算出 "ABCABDEF" 的前缀表为 [0, 0, 0, 1, 2, 0, 0, 0]"AABAAAB" 的前缀表为 [0, 1, 0, 1, 2, 2, 3]"ABCDABD" 的前缀表为 [0, 0, 0, 0, 1, 2, 0]

在之前的例子中,当 p[5]T[i + 5] 匹配失败后,根据模式串失配位置 j 的前一个位置的值,即 next[4] = 2,我们直接让 T[i + 5] 直接对准了 p[2],然后继续进行比对,如下图所示。

但是这样移动的原理是什么?

其实在上文 「1.2 KMP 算法的改进」 中的例子中我们提到过了。现在我们将其延伸总结一下,其实这个过程就是利用了前缀表进行模式串移动的原理,具体推论如下。

如果文本串 T[i: i + m] 与模式串 p 的失配是在第 j 个下标位置发生的,那么:

  • 文本串 T 从下标位置 i 开始连续的 j 个字符,一定与模式串 p 的前 j 个字符一模一样,即:T[i: i + j] == p[0: j]
  • 而如果模式串 p 的前 j 个字符中,前 k 位前缀和后 k 位后缀相同,即 p[0: k] == p[j - k: j],并且要保证 k 要尽可能长。

可以推出:文本串子串的后 k 位后缀和模式串子串的前 k 位是相同的,即 T[i + m - k: i + m] == p[0: k](这部分是已经比较过的),不需要再比较了,可以直接跳过。

那么我们就可以将文本串中的 T[i + m] 对准模式串中的 p[k],继续进行对比。这里的 k 其实就是 next[j - 1]

2. KMP 算法步骤

3.1 next 数组的构造

我们可以通过递推的方式构造 next 数组。

  • 我们把模式串 p 拆分成 leftright 两部分。left 表示前缀串开始所在的下标位置,right 表示后缀串开始所在的下标位置,起始时 left = 0right = 1
  • 比较一下前缀串和后缀串是否相等。通过比较 p[left]p[right] 来进行判断。
  • 如果 p[left] != p[right],说明当前的前后缀不相同。则让后缀开始位置 k 不动,前缀串开始位置 left 不断回退到 next[left - 1] 位置,直到 p[left] == p[right] 为止。
  • 如果 p[left] == p[right],说明当前的前后缀相同,则可以先让 left += 1,此时 left 既是前缀下一次进行比较的下标位置,又是当前最长前后缀的长度。
  • 记录下标 right 之前的模式串 p 中,最长相等前后缀的长度为 left,即 next[right] = left

3.2 KMP 算法整体步骤

  1. 根据 next 数组的构造步骤生成「前缀表」next
  2. 使用两个指针 ij,其中 i 指向文本串中当前匹配的位置,j 指向模式串中当前匹配的位置。初始时,i = 0j = 0
  3. 循环判断模式串前缀是否匹配成功,如果模式串前缀匹配不成功,将模式串进行回退,即 j = next[j - 1],直到 j == 0 时或前缀匹配成功时停止回退。
  4. 如果当前模式串前缀匹配成功,则令模式串向右移动 1 位,即 j += 1
  5. 如果当前模式串 完全 匹配成功,则返回模式串 p 在文本串 T 中的开始位置,即 i - j + 1
  6. 如果还未完全匹配成功,则令文本串向右移动 1 位,即 i += 1,然后继续匹配。
  7. 如果直到文本串遍历完也未完全匹配成功,则说明匹配失败,返回 -1

3. KMP 算法代码实现

# 生成 next 数组
# next[j] 表示下标 j 之前的模式串 p 中,最长相等前后缀的长度
def generateNext(p: str):m = len(p)next = [0 for _ in range(m)]                # 初始化数组元素全部为 0left = 0                                    # left 表示前缀串开始所在的下标位置for right in range(1, m):                   # right 表示后缀串开始所在的下标位置while left > 0 and p[left] != p[right]: # 匹配不成功, left 进行回退, left == 0 时停止回退left = next[left - 1]               # left 进行回退操作if p[left] == p[right]:                 # 匹配成功,找到相同的前后缀,先让 left += 1,此时 left 为前缀长度left += 1next[right] = left                      # 记录前缀长度,更新 next[right], 结束本次循环, right += 1return next# KMP 匹配算法,T 为文本串,p 为模式串
def kmp(T: str, p: str) -> int:n, m = len(T), len(p)next = generateNext(p)                      # 生成 next 数组j = 0                                       # j 为模式串中当前匹配的位置for i in range(n):                          # i 为文本串中当前匹配的位置while j > 0 and T[i] != p[j]:           # 如果模式串前缀匹配不成功, 将模式串进行回退, j == 0 时停止回退j = next[j - 1]if T[i] == p[j]:                        # 当前模式串前缀匹配成功,令 j += 1,继续匹配j += 1if j == m:                              # 当前模式串完全匹配成功,返回匹配开始位置return i - j + 1return -1                                   # 匹配失败,返回 -1print(kmp("abbcfdddbddcaddebc", "ABCABCD"))
print(kmp("abbcfdddbddcaddebc", "bcf"))
print(kmp("aaaaa", "bba"))
print(kmp("mississippi", "issi"))
print(kmp("ababbbbaaabbbaaa", "bbbb"))

4. KMP 算法分析

  • KMP 算法在构造前缀表阶段的时间复杂度为 O ( m ) O(m) O(m),其中 m m m 是模式串 p 的长度。
  • KMP 算法在匹配阶段,是根据前缀表不断调整匹配的位置,文本串的下标 i 并没有进行回退,可以看出匹配阶段的时间复杂度是 O ( n ) O(n) O(n),其中 n n n 是文本串 T 的长度。
  • 所以 KMP 整个算法的时间复杂度是 O ( n + m ) O(n + m) O(n+m),相对于朴素匹配算法的 O ( n ∗ m ) O(n * m) O(nm) 的时间复杂度,KMP 算法的效率有了很大的提升。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/747700.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用 ZipArchiveInputStream 读取压缩包内文件总数

读取压缩包内文件总数 简介 ZipArchiveInputStream 是 Apache Commons Compress 库中的一个类,用于读取 ZIP 格式的压缩文件。在处理 ZIP 文件时,编码格式是一个重要的问题,因为它决定了如何解释文件中的字符数据。通常情况下,Z…

代码随想录刷题笔记 Day 52 | 打家劫舍 No.198 | 打家劫舍 II No.213 | 打家劫舍III No.337

文章目录 Day 5201. 打家劫舍&#xff08;No. 198&#xff09;<1> 题目<2> 笔记<3> 代码 02. 打家劫舍 II&#xff08;No. 213&#xff09;<1> 题目<2> 笔记<3> 代码 03.打家劫舍III&#xff08;No. 337&#xff09;<1> 题目<2&g…

工智能的迷惑是技术发展的产物

简述&#xff1a; 随着ChatGPT在全球科技舞台上掀起一股热潮&#xff0c;人工智能再次成为了人们关注的焦点。各大公司纷纷紧跟潮流&#xff0c;推出了自己的AI大模型&#xff0c;如&#xff1a;文心一言、通义千问、讯飞星火、混元助手等等&#xff0c;意图在人工智能领域占据…

Vue多文件学习项目综合案例——购物车,黑马vue教程

一、项目截图 二、主要知识点 vuex的使用json-server的使用json-server --watch index.json三、需要注意的点 json-server 安装成功&#xff0c;查看版本直接报错。安装默认版本埋下的一个坑&#xff0c;和node版本不匹配作者直接安装vuex&#xff0c;默认安装也是版本不匹配…

鸿蒙Harmony应用开发—ArkTS声明式开发(容器组件:Panel)

可滑动面板&#xff0c;提供一种轻量的内容展示窗口&#xff0c;方便在不同尺寸中切换。 说明&#xff1a; 该组件从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 子组件 可以包含子组件。 说明&#xff1a; 子组件类型&a…

QMI8658芯片I2C驱动开发指南

这个芯片纯国产挺好用的&#xff0c;电路很好设计&#xff0c;我这垃圾焊功&#xff0c;纯手焊&#xff0c;&#xff0c;居然能用。 第一部分 硬件连接 画的很简陋&#xff0c;看看就可以了&#xff0c;这里I2C总线需要接10K上拉没有画出来&#xff0c;这个需要注意一下。 …

树形结构 一篇文章梳理

树形结构是一种非常重要的非线性数据结构&#xff0c;它模拟了具有层次关系的数据模型。在树形结构中&#xff0c; 目录 一、组成元素&#xff1a; 二、树的属性&#xff1a; 深度或高度 度 路径 路径长度 三、树的类型 1 二叉树 2 多叉树 3 完全二叉树 4 满二叉树…

【计算机网络_传输层】UDP和TCP协议

文章目录 1. 重新理解端口号端口号划分netstat指令pidof 2. UDP协议2.1 UDP协议端格式2.2 UDP的特点2.3 UDP的注意事项2.4 基于UDP的应用层协议 3. TCP协议&#xff08;传输控制协议&#xff09;3.1 TCP协议的格式和报头字段3.2 如何解包和分用3.3 理解TCP协议报头3.4 TCP协议的…

day-20 二叉树的层序遍历

思路&#xff1a;利用队列进行广度优先遍历即可 注意点&#xff1a;ArrayList执行remove之后&#xff0c;索引i会立即重排&#xff0c;注意可能越界 code: /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeN…

Xcode调试Qt 源码

在Mac下使用Xcode 开发Qt程序&#xff0c;由于程序断点或者崩溃后&#xff0c;Qt库的堆栈并不能够正确定位到源码的cpp文件&#xff0c;而是显示的是汇编代码&#xff0c;导致不直观的显示。 加载的其他三方库都是同理。 所以找了攻略和研究后&#xff0c;写的这篇文章。 一&a…

SIP调试之SIPP测试工具

SIPP是针对SIP协议的一个性能测试的命令行工具&#xff0c;可以动态显示测试的统计信息&#xff08;如呼叫速率、延时、消息统计等&#xff09;。用户可以通过XML场景配置文件&#xff0c;自定义模拟各种UAC/UAS测试场景的信令交互流程&#xff0c;可以被用来测试IP话机、SIP代…

【IC设计】Verilog线性序列机点灯案例(二)(小梅哥课程)

文章目录 该系列目录&#xff1a;设计目标设计思路RTL 及 Testbench仿真结果存在的问题&#xff1f;改善后的代码RTL代码testbench代码 仿真结果 案例和代码来自小梅哥课程&#xff0c;本人仅对知识点做做笔记&#xff0c;如有学习需要请支持官方正版。 该系列目录&#xff1a;…

Nexus如何导入jar以及批量导入Maven的本地库目录

上传依赖包到Nexus 服务器的方式有多种, 包含: 1.单个jar上传: 在Nexus管理台页面上传单个jar 2.源码编译上传:在源码项目中使用 Maven的deploy 命令发布 3. 使用脚本批量上传Maven本地库的目录 前言 本篇基于 Nexus 的版本是 nexus-3.55.0-01本方法适用Linux和WindowsWind…

MySQL-HMA 高可用故障切换

本章内容&#xff1a; 了解MySQL MHA搭建MySQL MHAMySQL MHA故障切换 1.案例分析 1.1.1案例概述 目前 MySQL 已经成为市场上主流数据库之一&#xff0c;考虑到业务的重要性&#xff0c;MySQL 数据库 单点问题已成为企业网站架构中最大的隐患。随着技术的发展&#xff0c;MHA…

【四 (2)数据可视化之 Matplotlib 常用图表及代码实现 】

目录 文章导航一、介绍二、安装Matplotlib三、导入Matplotlib四、设置可以中文显示四、常用图形1、散点图&#xff08;Scatter Plot&#xff09;2.1、线性图&#xff08;Line Plot&#xff09;2.2、堆叠折线图2.3、多图例折线图3.1、柱状图/条形图&#xff08;Bar Chart&#x…

PostgreSQL中vacuum 物理文件truncate发生的条件

前言 前段时间&#xff0c;有些同学说到vacuum截断的行为时&#xff0c;认为&#xff0c;只要末尾是空页&#xff0c;无论多少&#xff0c;都会被截断&#xff0c;真是这样的吗&#xff1f; PostgreSQL当中&#xff0c;由于vacuum的操作并不总能将死元组的空间进行”物理截断…

HNU-计算机系统-实验1-原型机vspm1.0-(二周目玩家视角)

前言 二周目玩家&#xff0c;浅试一下这次的原型机实验。总体感觉跟上一年的很相似&#xff0c;但还是有所不同。 可以比较明显地感觉到&#xff0c;这个界面越来越好看了&#xff0c;可操作与可探索的功能也越来越多了。 我们HNU的SYSTEM真的越来越好了&#xff01;&#x…

JMeter 查看 TPS 数据,详细指南

TPS 是软件测试结果的测量单位。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时&#xff0c;收到服务器响应后结束计时&#xff0c;以此来计算使用的时间和完成的事务个数。在 JMeter 中&#xff0c;我们可以使用以下方法查看 T…

量子遗传算法优化VMD参数,五种适应度函数任意切换,最小包络熵、样本熵、信息熵、排列熵、排列熵/互信息熵...

关于量子遗传算法&#xff0c;在众多文献均有应用。下面简述一下原理。 &#xff08;1&#xff09;量子比特编码 子遗传算法通过引入量子比特来完成基因的存储和表达。量子比特是量子信息中的概念&#xff0c;它与经典比特不同&#xff0c;是因为它可以在同一时刻处于两个状态的…

Unreal发布Android App如何面对混乱的Android SDK开发环境

Unreal发布Android App如何面对混乱的Android SDK开发环境 混乱的Android SDK开发环境Unreal 4可以借用Unity3D安装的Android环境Unreal 5需要安装Android Studio开发环境Android Studio的DK版本目录处理gradle和java版本gradle提示错误总结 混乱的Android SDK开发环境 Unreal…