【算法】KMP算法

写在前面

在学习KMP算法前,不才也曾在众多博客中阅读过KMP算法的文章,但是都看得迷迷糊糊,所以不才在学透了KMP算法后,详细编写了这篇笔记,希望对你有帮助🥰🥰。

KMP算法的核心思想不分任何语言,但是不才在实现代码中时使用C语言实现。


一、什么是KMP算法

在学KMP算法前,我们首先需要知道KMP算法是什么,干什么用的(你滴什么滴干活🫡)。

KMP算法是:查找源字符串在目标字符串中出现的位置。并返回一个指向str1中str2第一次出现的指针,如果str2不是str1的一部分,则返回一个空指针。实际上就是与(strstr)相似,但是strstr是使用BF算法(暴力算法)进行查找的。

本篇笔记需要有BF算法的基础,这里可以参考不才写的字符函数和字符串函数笔记

模拟实现的strstr:【C语言】字符函数和字符串函数icon-default.png?t=O83Ahttps://blog.csdn.net/m0_71580879/article/details/142614046


二、为什么要使用KMP算法

假设我们str1的数组有n个元素str2的数组有m个元素。我们使用BF算法的时间复杂度就是O(n*m),如果是两数组个巨无霸的的情况,我们使用的BF算法就非常耗时。但是使用KMP算法我们就可以把时间复杂度变为:O(n+m)。若m与n相同的情况下:BF算法的时间复杂度O(n^2)、KMP算法时间复杂度为O(n)。这样我们的时间成本就大幅度下降。

三、KMP算法的核心思想

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

上文是一个压缩后很正确的表诉方式,但是为了更好理解上面这段话,不才在下面进行逻辑上的表诉。

假设我们主串str1的数组有n个元素,子串str2的数组有m个元素

KMP算法为了达到时间复杂度为:O(n+m)。那我们就需要遍历str1数组的指针不返回为了实现遍历str1数组的指针不返回,我们就需要创建一个数组next数组保存我们str2数组中如果出现错误我们需要str2数组回退的位置

而怎么计算next数组中的值就是我们KMP算法的核心逻辑了。


四、证明遍历主串str1数组的指针可以不返回

首先,我们先举例说明一下,为什么遍历主串str1数组的指针可以不返回。

例子一:

在上图中,我们使用BF算法比较的话,我们在str1和str2比较之前我们肯定需要一个dst1和一个dst2来定位比较前的位置的(如下图):我们先肉眼观察

当我们 i j 比较到下标为2时候,发现不匹配,那么我们的dst1就会+1指向b字符处。但是我们知道的是,str1[0] == str2[0]、str1[1] == str2[1]、str1[0] != str1[1] 可得str1[1] != str2[0] ,那么我们的dst1+1指向b字符处的比较大可不必的,我们dst可以直接在 i 处继续进行比较。这样就达成了我们遍历主串str1数组的指针可以不返回。

当然有朋友就回提出质疑了,就一个特殊案例可以说明了?你 j 怎么移动?啊?

别急 接着往下看😎


例子二: 

此时我们又有一个案例:

开始比较后,下标走到5后,出现了不匹配的情况:

在此时,我们没有出现元素各不相等的情况。

出现了:

  • str1[0]…str1[1] == str2[0]…str2[1] (ab == ab)
  • str1[3]…str1[4] ==  str2[0]…str2[1](ab == ab)

我们继续使用BF算法来理解,根据上面例一的推理,我们可以得出dst1需要加到下标为3处开始比较才有意义。但是上面的推理 str1[3]…str1[4] ==  str2[0]…str2[1] 可以看出,我们下标3、4都是相等的,那么我们的dst1又可以不进行回退了,保持在 i 的位置,我们只需要 j 下标退回下标2处开始比较即可。如下图:

那此时我们应该怎么知道我们指针 j 应该回退到哪个下标处呢?

 我们就需要创建一个数组next数组保存我们str2数组中如果出现错误我们需要 j 回退的位置


 五、next数组

KMP 的精髓就是 next 数组:也就是用 next[j] = k;来表示,目前 j 代表的是next数组的下标不同的 j 来对应一个 K 值, 这个 K 就是你将来要移动的 j 要移动的位置

5.1肉眼手搓next数组

手搓next数组即手搓K值,在此之前我们需要知道手搓K值规则是什么:

  1. 找到匹配成功部分的两个相等的真子串(不包含本身),一个以下标 0 字符开始,另一个以 j-1 下标 字符结尾。匹配成功那么 字串的长度 就等于 k值。
  2. 不管什么数据 next[0] = -1;next[1] = 0;在这里,我们以下标来开始,而说到的第几个第几个是从 1 开始;

我们使用第四小节的例子二为栗子:

手搓K值:

  • 上来不管三七二十一先把: next[0] = -1 next[1] = 0
  • next[2]下标为 2 时str2[0]:a 与 str2[j - 1] 即 str2[2 - 1] ==> str2[1]:b。那么就找以 a 开始b结尾两个连续相等的字串。明显没有~,那么 next[2] = 0;
  • next[3]下标为 3 时:找以 a 开始c结尾两个连续相等的字串。只有本身(abc一个字串),那k值为0,那么 next[3] = 0;
  • next[4]下标为 4 时:找以 a 开始a结尾两个连续相等的字串。a开头a结尾,那a就有两个字串呀( str2[0]...str2[0] == str2[3]...str2[3] ),那么k就为1,即 next[4] = 1;(下图)

  • next[5]下标为 5 时:找以 a 开始b结尾两个连续相等的字串。a开头a结尾,那ab就有两个字串呀( str2[0]...str2[1] == str2[3]...str2[4] ),那么k就为 2,即 next[5] = 2;(下图)

由上面手搓可以得出:next数组为:next[5] = {-1, 0, 0, 0, 1, 2};对应着str2的 j 指针在哪个位置出错就返回next对应的位置

譬如:

  • j下标为5时比较错误。返回值为:next[5]
  • j下标为 2 时比较错误。返回值为:next[2]

 手搓K值我们理解了如何手搓,那么在程序中,代码根本就不会想我们人眼一样肉眼手搓出来,我们要怎么计算出K值呢?

5.2计算K值

重要结论:

  1. str[i - 1] == str[K](K == next[i-1]),我们可以直接把next数组的i下标赋值为 K+1。即:next[ i ] = k + 1;
  2. str[i - 1] != str[K](K == next[i-1]),我们需要K值移动到str[K]处的K值,即K = next[K],在K值重新赋值定位后,我们再把str[i - 1] 与 str[K]进行比较直到               str[i - 1] == str[K]。或K为-1
  3. str[i - 1] != str[K]时,我们循环比较没有成功str[i - 1] == str[K]。那K一定会为-1,在K为-1时已经没有所对应匹配的字符了,所以直接把下标 i 处的next数组值赋值为:0。即next[i] = 0

我们使用逻辑推理:在5.1例子中 next数组为:next[5] = {-1, 0, 0, 0, 1, 2};

此时我们定义一个变量 int i = 0;

若i = 4时, 那next数组中有:str2[0]...str2[0] == str2[3]...str2[3],即next[4] = 1;

此时,我们把变量名称代入可以得到一个表达式:str2[0]...str2[0] == str2[3]...str2[3] ==> str2[0]...str2[?] == str2[?]...str2[i - 1]

且:k = next[i] ;  (i = 4, k = 1),即可得到下图:

计算问好:

  • 问好一:我们知道是由下标0到0拼接成第一个字串,即第一个字串a。在上图中我们可以看到第一个问好就是: k - 1 即:str2[0]...str2[k - 1] == str2[?]...str2[i - 1]
  • 问好二:我们知道是由下标 3 到 3 拼接成第二个字串,即为第二个字串a。那第二个问好为: x。此时,x是未知的,但是我们知道:str2[0]...str2[k - 1] == str2[x]...str2[i - 1]绝对成立

因为长度是一定相等的,那么可以推出k-1 - 0 == i-1 - x。是恒成立的。

k-1 - 0 == i-1 - x  =>  k-1 = i-1 - x  =>  x = i-1 - k+1  即   x = i - k

我们就得出求 k 值重要公式str2[0]...str2[k - 1] == str2[i - k]...str2[i - 1] 

如果此时我们假设不知下标为 5 的k值,求i = 5时K值为多少

当 i= 5 时,我们观察上图可以发现,str2[i - 1] == str2[K](此时的 K 值时对应着 i - 1 的K值,K=1)。

str2[4] == str2[1],在str2[i - 1] == str2[K]相等的情况下(使用重要结论1),我们把 i 值代入得:str2[i - 1] == str2[K]下标为5 next数组的k值是 i - 1 下标的k值加1。即:next[5] = 2

总结:我们虽然是举了个特例证明,当str2[i - 1] = str2[K]时,next[i] = K + 1。(此时的 K 值时对应着 i - 1 的K值)。

我们举个栗子二来说明,当 str2[i - 1] != str2[K]应该怎么操作

例子二:

 在上面数组中,我们使用计算的方法把next数组计算出来。

  • 首先不管三七二十一,next[0] = -1 与 next[1] = 0,先录入数组
  • 此时我们就遇到str1[i - 1] != str1[K]的情况。

str1[i - 1] != str1[K]时,我们只需要把 K 移动到当前str1[K]所对应的next数组K值即可,即把此时的str[K]的 next[1] 赋值个 K,即为:K = next[K],那么K就移动到了下标0处。(如下图)

那此时再继续判断str1[i - 1]是否str1[K]相等。此时我们还是不相等,那K继续移动K = next[K]。但是此时next[K]等于 -1 ,已经没有所对应匹配的字符了,所以直接把下标为2处的next数组值赋值为:0

那此时我们i下标继续向后移动,K值重新赋值到next[i - 1]处(此时next[2] = 0)。再来判断str1[i - 1]是否与str1[K]相等

此时:str1[i - 1] 等于 cstr1[K]等于a,此时又回到了我们str1[i - 1] != str1[K]的情况,那么我们继续把 K 移动当前str1[K]所对应的next数组K值,继续比较,但是此时next[K]等于 -1 ,已经没有所对应匹配的字符了,所以直接把下标为3处的next数组值赋值为:0。即next[i] = 0。如下图

那此时我们i下标继续向后移动,K值重新赋值到next[i - 1]处(此时next[3] = 0)。再来判断str1[i - 1]是否与str1[K]相等

此时:str1[i - 1] 等于 a,str1[K]也等于a,那就回到了我们 str1[i - 1] == str1[K] 相等的时候,那此时我们next的表达式就为:next[i] = K+1,即next[4] = 0+1 => next[4] = 1。可得下图

那此时我们i下标继续向后移动,K值重新赋值到next[i - 1]处(此时next[4] = 1)。再来判断str1[i - 1]是否与str1[K]相等

此时:str1[i - 1] 等于 b,str1[K]也等于b,那就回到了我们 str1[i - 1] = str1[K] 相等的时候,那此时我们next的表达式就为:next[i] = K+1,即next[5] = 1+1 => next[5] = 2。可得下图

以上面逻辑继续我们最终就可以得到next数组的全部内容:

至此,我们KMP算法的next数组求值方法我们就全部掌握了。

5.3总结

我们要实现KMP算法的时间复杂度可以达到O(m+n),我们就得确保主串的遍历指针 j 不回退,为了主串的遍历指针i不回退,我们就要字串的遍历数组 i 配合i 需要回退到特定位置,而next数组就是存放字串 i 再当前下标比较失败后需要回退的位置

在next中,我们需要计算出每一个下标对应的K值,而K值的计算正时KMP算法的难点,我们需要分类讨论

  • str[i - 1] == str[K](K == next[i-1]),我们可以直接把next数组的i下标赋值为 K+1。即:next[ i ] = k + 1;
  • str[i - 1] != str[K](K == next[i-1]),我们需要K值移动到str[K]处的K值,即K = next[K],在K值重新赋值定位后,我们再把str[i - 1] 与 str[K]进行比较直到               str[i - 1] == str[K]。或K为-1

  • str[i - 1] != str[K]时,我们循环比较没有成功str[i - 1] == str[K]。那K一定会为-1,在K为-1时已经没有所对应匹配的字符了,所以直接把下标 i 处的next数组值赋值为:0。即next[i] = 0

六、思想转换代码

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>void my_next(const char* str2, int* next) {assert(str2 && next);next[0] = -1;next[1] = 0;int i = 2;//str数组只需要从下标为2处开始比较,因为下标0、1的next数组都是默认值int K = next[i - 1];while (str2[i]) {//判断当前str数组是否为结束符标志'\0'if (str2[i - 1] == str2[K] || K == -1) {//判断str2的i-1处的值是否与K处的值相等 ,或者K等于-1时进来改变赋值next[i] = K + 1;i++;K = next[i - 1];}else {K = next[K];}}
}char* my_KMP(const char* str1,char* str2) {assert(str1 && str2);int len1 = strlen(str1);int len2 = strlen(str2);int* next = (int* )malloc(len2 * sizeof(int));if (next == NULL) {perror("my_KMP的next开辟空间:>");return;}my_next(str2, next);//创建好next数组int i = 0;int j = 0;while (i < len1) {if (str1[i] == str2[j] || j == -1) {i++;j++;}else{j = next[j];}if (j == len2) {return str1 + i - j;}}free(next);next = NULL;return NULL;
}int main() {char arr1[] = "1233321123";char arr2[] = "33";char* ptr = my_KMP(arr1, arr2);printf("%s", ptr);return 0;
}
  • 我们先从main函数开始,我们需要实现一个KMP嘛。我们创建一个KMP函数命名为:my_KMP函数
  • my_KMP函数中我们先创建next数组,在创建next数组后,我们就需要把字串的K值全部计算并赋值到next数组中,这时我们就创建一个my_next函数用来计算字串的K值
  • 在my_next数组中,我们使用 5.3总结知识转换成代码,来实现存储K值到对应的next数组中。在一上来我们肯定不管三七二十一就先把next数组的0、1下标赋值为:next[0] = -1;
    next[1] = 0;
    😎之后我们就把 5.3总结实现。😎
  • 在存储好了next数组后,我们就实现比较逻辑,在比较中,我们是需要主串的 i 不返回i 值就只能在 匹配成功 字串的下标为 -1时 才能后移。在匹配不成功时,我们就需要 i 下标不移动j 移动到next数组对应的值中,继续比较,在j == len2时说明比较成功,就返回str1数组的对应地址。
  • 对应地址的计算: 主串移动的下标 i 减 字串的下标 j 就得到主串匹配成功的起始位置,str1 加上 匹配成功的起始位置 就可以得到 具体的地址。

以上就是本章所有内容。若有勘误请私信不才。万分感激💖💖 若有帮助不要吝啬点赞哟~~💖💖

ps:表情包来自网络,侵删🌹

若是看了这篇笔记彻底拿捏KMP算法的就可以在评论区打出:小小KMP!拿捏!😎

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

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

相关文章

二叉树习题其二Java【力扣】【算法学习day.9】

前言 前言 书接上篇文章二叉树习题其一&#xff0c;这篇文章我们将基础拓展 ###我做这类文档一个重要的目的还是给正在学习的大家提供方向&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思…

云计算第四阶段-----CLOUND二周目 04-06

cloud 04 今日目标&#xff1a; 一、Pod 生命周期 图解&#xff1a; [rootmaster ~]# vim web1.yaml --- kind: Pod apiVersion: v1 metadata:name: web1 spec:initContainers: # 定义初始化任务- name: task1 # 如果初始化任务失败&#…

目前最新 dnSpy V6.5.1版本,最好的 .NET 程序调试、编辑、反编译软件

目前最新 dnSpy V6.5.1版本&#xff0c;最好的 .NET 程序调试、编辑、反编译软件 一、 简介二、新发布程序更新功能三、官方下载&#xff1a; 一、 简介 dnSpy 是一个调试器 .NET 程序集的编辑器。即使没有源代码&#xff0c;也可以使用它来编辑和调试程序集。主要特点&#x…

连锁收银系统

商淘云连锁管理系统助力连锁企业实现“人货账”全方位数字化管理&#xff0c;它依托连锁品牌进销存管理实现门店订货、线下收银、线上商城、会员营销等一体化管理。 门店订货补货支持连锁直营、加盟 不同门店不同进货价、不同门店不同商品、不同门店在线或者账期支付、门店PC或…

分布式---raft算法

1、Leader的选举和Failover过程 首先了解raft中节点的三种状态&#xff1a; 1、Follower&#xff1a;Follower是请求的被动更新者&#xff0c;从Leader接收更新请求&#xff0c;将日志写入到本地文件2、Candidate:如果Follower在一定时间内&#xff0c;如果每收到Leader的心跳…

uniapp 获取签名证书 SHA1 自有证书签名打包

1.登录你的Dcloud 账户 2.找到我的应用菜单 3.点开某个应用 4.查看证书详情&#xff0c;里面有SHA1 和别名&#xff0c;密码&#xff0c;下载证书用于云打包&#xff0c;可以选择自有证书&#xff0c;输入别名&#xff0c;密码打包

GPT+Python)近红外光谱数据分析与定性/定量建模技巧

2022年11月30日&#xff0c;可能将成为一个改变人类历史的日子——美国人工智能开发机构OpenAI推出了聊天机器人ChatGPT3.5&#xff0c;将人工智能的发展推向了一个新的高度。2023年4月&#xff0c;更强版本的ChatGPT4.0上线&#xff0c;文本、语音、图像等多模态交互方式使其在…

高效实现用友BIP与旺店通数据无缝对接

用友BIP与旺店通企业奇门的YS其他入库单数据集成方案 在企业日常运营中&#xff0c;数据的高效流转和准确对接是确保业务顺畅运行的关键。本文将分享一个具体的系统对接案例&#xff0c;即如何将用友BIP平台中的YS其他入库单数据集成到旺店通企业奇门中&#xff0c;实现两大系…

简历修订与求职经历 - Chap05

现在是又一个周一。上周最值得记录的有这么几件事&#xff1a; 1.拿到了D照。 周二科目一&#xff0c;周四科目二三四联考——是打算为逆行人生准备的。有备无患。然后拿到驾照后发现又有一些问题。看了honda的125&#xff0c;感觉车身好胖。我不喜欢这类很胖的车。然后按照驾…

光伏行业如何借助ERP领跑绿色经济?

在全球能源结构转型和绿色能源转型的大背景下&#xff0c;现在光伏行业呈现出技术创新、市场需求扩大、产能调整和竞争加剧等特点&#xff0c;也预示行业的持续成长和未来的发展潜力。但企业仍然需要不断提高技术水平和管理水平以应对激烈的市场竞争&#xff0c;SAP ERP制定符合…

Maven基于构建阶段分析多余的依赖

基于构建阶段 test compile 实现依赖分析 执行maven 命令: mvn dependency:analyze 关注:Maven-dependency-plugin 分析结果: [INFO] --- maven-dependency-plugin:2.10:analyze (default-cli) impl --- 配置依赖未使用的依赖项&#xff1a; [INFO] --- maven-dependency-…

Lucas带你手撕机器学习——线性回归

什么是线性回归 线性回归是机器学习中的基础算法之一&#xff0c;用于预测一个连续的输出值。它假设输入特征与输出值之间的关系是线性关系&#xff0c;即目标变量是输入变量的线性组合。我们可以从代码实现的角度来学习线性回归&#xff0c;包括如何使用 Python 进行简单的线…

git的安装以及入门使用

文章目录 git的安装以及入门使用什么是git&#xff1f;git安装git官网 git初始化配置使用方式初始化配置&#xff1a; git的安装以及入门使用 什么是git&#xff1f; Git 是一个免费开源的分布式版本控制系统&#xff0c;使用特殊的仓库数据库记录文件变化。它记录每个文件的…

WebGl 使用uniform变量动态修改点的颜色

在WebGL中&#xff0c;uniform变量用于在顶点着色器和片元着色器之间传递全局状态信息&#xff0c;这些信息在渲染过程中不会随着顶点的变化而变化。uniform变量可以用来设置变换矩阵、光照参数、材料属性等。由于它们在整个渲染过程中共享&#xff0c;因此可以被所有使用该着色…

嵌入式linux系统中多路复用和信号驱动实现

大家好,今天主要给大家分享一下,如何使用linux系统中的多路复用和信号驱动的功能实现。 第一:linux多路复用基本特点 当应用程序同时处理多路数据的输入或输出时,若采用非阻塞模式,将达不到预期的效果 如果采用非阻塞模式,对多个输入进行轮询可以实现,但CPU的消耗非常大…

【设计模式系列】装饰器模式

目录 一、什么是装饰器模式 二、装饰器模式中的角色 三、装饰器模式的典型应用场景 四、装饰器模式在BufferedReader中的应用 一、什么是装饰器模式 装饰器模式是一种结构型设计模式&#xff0c;用于在不修改对象自身的基础上&#xff0c;通过创建一个或多个装饰类来给对象…

黑马 | Reids | 基础篇

黑马reids基础篇 文章目录 黑马reids基础篇一.初始Redis1.1SQL 和 NoSql的区别1.1.1结构化和非结构化1.1.2关联和非关联1.1.3查询方式1.1.4 事务1.1.5总结 1.2 认识Redis1.3 Redis安装启动默认启动&#xff1a;后台启动&#xff1a;开机自启 1.4 Redis客户端1.4.1.Redis命令行客…

windows安装mysql,跳过自定义的密码验证

1、mysql版本8 2、配置系统环境变量 3、新建my.ini文件在mysql目录下&#xff0c;需要指定data目录 [mysqld] # 设置3306端口 port3306# 自定义设置mysql的安装目录&#xff0c;即解压mysql压缩包的目录 basedirD:\hjl\app\mysql\mysql-8.0.33-winx64# 自定义设置mysql数据…

24/10/14 算法笔记 循环神经网络RNN

RNN: 一种专门用于处理序列数据的神经网络&#xff0c;它能够捕捉时间序列中的动态特征。RNN的核心特点是其循环连接&#xff0c;这允许网络在不同时间步之间传递信息&#xff0c;从而实现对序列数据的记忆和处理能力。 应用的场景&#xff1a; 自然语言处理&#xff08;NLP&…

[241021] X-CMD 内测版 v0.4.12 新功能: starship ohmyposh ping tping docker ascii

目录 X-CMD 发布内测版 v0.4.12&#x1f4c3;Changelog&#x1f3a8; starship&#x1f3a8; ohmyposh&#x1f3a8; theme&#x1f310; ping&#x1f310; tping&#x1f40b; docker&#x1f4bb; mac - 集成 MacOS 实用功能&#x1f504; ascii&#x1f996; deno&#x1f…