C++刷题 -- KMP算法

C++刷题 – KMP算法

文章目录

  • C++刷题 -- KMP算法
    • 1.算法讲解
    • 2.算法实现


https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/description/

1.算法讲解

KMP算法是一种字符串匹配算法,当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配;

前缀表(prefix table):
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

  • 举一个例子:
    要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。请添加图片描述
    可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。
    但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。

  • 前缀表是如何记录的:
    首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置
    前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀

  • 最长公共前后缀:
    字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串
    后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
    前缀表要求的就是相同前后缀的长度;
    字符串a的最长相等前后缀为0。 字符串aa的最长相等前后缀为1。 字符串aaa的最长相等前后缀为2。 等等…

  • 为什么一定要用前缀表
    刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图:请添加图片描述
    然后就找到了下标2,指向b,继续匹配:如图
    请添加图片描述
    下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
    所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。

  • 如何计算前缀表
    如图:
    请添加图片描述
    长度为前1个字符的子串a,最长相同前后缀的长度为0。(注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。)
    请添加图片描述
    长度为前2个字符的子串aa,最长相同前后缀的长度为1;
    请添加图片描述
    长度为前3个字符的子串aab,最长相同前后缀的长度为0
    以此类推: 长度为前4个字符的子串aaba,最长相同前后缀的长度为1。 长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。 长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。
    那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:
    请添加图片描述
    可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀
    再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:
    请添加图片描述
    找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少,因为要找前面字符串的最长相同的前缀和后缀
    前一个字符的前缀表的数值是2, 所以把下标移动到下标2的位置继续比配。
    最后就在文本串中找到了和模式串匹配的子串了。

  • 前缀表与next数组
    很多KMP算法的实现都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?
    next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。
    其实这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)

2.算法实现

  • 使用next数组来匹配
    以下我们以前缀表统一减一之后的next数组来做演示。
    有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。
    注意next数组是新前缀表(旧前缀表统一减一了)。
    匹配过程动画如下请添加图片描述

  • 时间复杂度分析
    其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
    暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。
    为了和力扣题目28.实现strStr保持一致,方便大家理解,以下文章统称haystack为文本串, needle为模式串

  • 构造next数组
    定义一个函数getNext来构建next数组,函数参数为指向next数组的指针,和一个字符串。
    在这里插入图片描述
    构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:

    1. 初始化:
      定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置
      然后还要对next数组进行初始化赋值,如下:
      在这里插入图片描述
      j 为什么要初始化为 -1呢,因为之前说过 前缀表要统一减一的操作仅仅是其中的一种实现,我们这里选择j初始化为-1,下文我还会给出j不初始化为-1的实现代码。
      next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)
      所以初始化next[0] = j 。

    2. 处理前后缀不相同的情况:
      因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较
      所以遍历模式串s的循环下标i 要从 1开始,代码如下:
      在这里插入图片描述
      如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。
      怎么回退呢?
      next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。
      那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。
      在这里插入图片描述
      每次求的都是当前字符串开头到i之间的子串的最大公共前后缀
      因此j指向子串的一个前缀末尾位置,i指向子串的一个后缀末尾位置
      例如:
      在这里插入图片描述
      上图中判断的是aab这个子串的前后缀,显然没有相同的前后缀,因此next[i]为0;
      在这里插入图片描述
      上图判断的是aabaa这个子串的前后缀,j+1指向的是当前的前缀aa的末尾,i指向的是当前的后缀aa的末尾,显然前后缀末尾是一致的,因此next[i] = j = 1;

    3. 处理前后缀相同的情况:
      如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。
      在这里插入图片描述

构建next数组的逻辑:
请添加图片描述

最后整体构建next数组的函数代码如下:

void getNext(int* next, const string& s){int j = -1;next[0] = j;for(int i = 1; i < s.size(); i++) { // 注意i从1开始while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了j = next[j]; // 向前回退}if (s[i] == s[j + 1]) { // 找到相同的前后缀j++;}next[i] = j; // 将j(前缀的长度)赋给next[i]}
}
  • 使用next数组来做匹配
    在文本串s里 找是否出现过模式串t。
    定义两个下标j 指向模式串起始位置i指向文本串起始位置
    那么j初始值依然为-1,为什么呢? 依然因为next数组里记录的起始位置为-1。
    i就从0开始,遍历文本串,代码如下:
    在这里插入图片描述
    接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 进行比较。
    如果 s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置。
    代码如下:
    在这里插入图片描述
    如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动, 代码如下:
    在这里插入图片描述
    如何判断在文本串s里出现了模式串t呢,如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。
    本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。
    那么使用next数组,用模式串匹配文本串的整体代码如下:
int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // 注意i就从0开始while(j >= 0 && s[i] != t[j + 1]) { // 不匹配j = next[j]; // j 寻找之前匹配的位置}if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动j++; // i的增加在for循环里}if (j == (t.size() - 1) ) { // 文本串s里出现了模式串treturn (i - t.size() + 1);}
}

KMP算法的整体代码:

class Solution {
public:void getNext(int* next, const string& s) {int j = -1;next[0] = j;for(int i = 1; i < s.size(); i++) { // 注意i从1开始while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了j = next[j]; // 向前回退}if (s[i] == s[j + 1]) { // 找到相同的前后缀j++;}next[i] = j; // 将j(前缀的长度)赋给next[i]}}int strStr(string haystack, string needle) {if (needle.size() == 0) {return 0;}int next[needle.size()];getNext(next, needle);int j = -1; // // 因为next数组里记录的起始位置为-1for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配j = next[j]; // j 寻找之前匹配的位置}if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动j++; // i的增加在for循环里}if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串treturn (i - needle.size() + 1);}}return -1;}
};
  • 时间复杂度: O(n + m)
  • 空间复杂度: O(m), 只需要保存字符串needle的前缀表

前缀表不减1

class Solution {
public:void getNext(int* next, const string& s){int j = 0;//字符串当前检查的前缀的末尾next[0] = j;//初始化for(int i = 1; i < s.size(); i++){//前后缀两个子串都是从大小为1的子串开始检查的//如果前缀后缀末尾不一致,就对j进行回退,看回退后能否匹配while(j > 0 && s[i] != s[j]){j = next[j - 1];//next中记录着已经检查到的相同前后缀的长度}//如果前后缀末尾一致,就对j++if(s[i] == s[j]){j++;}//当出现一直的前后缀末尾,就需要在next表中记录当前的相同前后缀长度next[i] = j;}}int strStr(string haystack, string needle) {if(needle.size() == 0){return -1;}int next[needle.size()];getNext(next, needle);int j = 0; // 指向needle的指针for(int i = 0; i < haystack.size(); i++){//判断当前两指针所指字符是否相等,不相等就回退jwhile(j > 0 && haystack[i] != needle[j]){j = next[j - 1];}//如果一致,就对j++if(haystack[i] == needle[j]){j++;}//如果j遍历到了needle的末尾,就说明存在子串if(j == needle.size()){return i - needle.size() + 1;}}return -1;}
};

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

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

相关文章

2024 年 8 个顶级开源 LLM(大语言模型)

如果没有所谓的大型语言模型&#xff08;LLM&#xff09;&#xff0c;当前的生成式人工智能革命就不可能实现。LLM 基于 transformers&#xff08;一种强大的神经架构&#xff09;是用于建模和处理人类语言的 AI 系统。它们之所以被称为“大”&#xff0c;是因为它们有数亿甚至…

Python tkinter 初探Toplevel控件搭建父子窗口

目录 Toplevel控件搭建父子窗口 最简明的父子窗口框架 改进一&#xff1a;屏蔽和开放按钮 改进二&#xff1a;子窗口始终在主窗口之上 改进三&#xff1a;增加子窗口的关闭协议 改进四&#xff1a;使子窗口长获焦点 总结 Toplevel控件搭建父子窗口 最近&#xff0c;用P…

EasyExcel合并相同内容单元格及动态标题功能的实现

一、最初版本 导出的结果&#xff1a; 对应实体类代码&#xff1a; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.write.style.ColumnWidth; import com.alibaba.excel.annotation.write.style.ContentLoopMerge; import com.al…

【TB作品】51单片机,具有报时报温功能的电子钟

2.具有报时报温功能的电子钟 一、功能要求: 1.显示室温。 2.具有实时时间显示。 3.具有实时年月日显示和校对功能。 4.具有整点语音播报时间和温度功能。 5.定闹功能,闹钟音乐可选。 6.操作简单、界面友好。 二、设计建议: 1.单片机自选(C51、STM32或其他单片机)。 2.时钟日历芯…

第十七章 爬虫scrapy登录与中间件2

文章目录 数据盘区太快会报错&#xff0c;setting中配置延迟 连接提取器

深度学习记录--矩阵维数

如何识别矩阵的维数 如下图 矩阵的行列数容易在前向和后向传播过程中弄错&#xff0c;故写这篇文章来提醒易错点 顺便起到日后查表改错的作用 本文仅作本人查询参考(摘自吴恩达深度学习笔记)

线上环境如何正确配置 Django 的 DEBUG?

Author&#xff1a;rab Django Version&#xff1a;3.2 Python Version&#xff1a;3.9 目录 前言一、DEBUG True二、DEBUG False三、页面异常解决总结 前言 由于最近在学习 Django 的知识&#xff0c;于是尝试开发了一套 Blog 系统&#xff0c;在本地测试时是页面显示没问题…

Windows 10如何关闭系统自动更新(实用教程)

本章教程&#xff0c;用最简洁的方式介绍在windows10中如何关闭系统自动更新。 目录 一、关闭自动更新服务 二、关闭自动更新组策略 一、关闭自动更新服务 1、 winr 2、services.msc 3、找到并双击 Windows Update 修改启动类型为禁用 二、关闭自动更新组策略 1、winr 2、gp…

el-form与el-upload结合上传带附件的表单数据(前端篇)

1.写在之前 本文前端采用Vue element-plus技术栈&#xff0c;前端项目参考yudao-ui-admin-vue3项目与Geeker-Admin项目。 这篇文章是el-form与el-upload结合上传带附件的表单数据&#xff08;后端篇&#xff09;-CSDN博客姐妹篇&#xff0c;后端篇文章主要讲的是后端的实现逻…

JavaEE:线程池精讲

目录 一.什么是线程池 二.线程池的实现原理 &#x1f388;为什么要有工厂模式&#xff1f; 三.线程池的构造方法解读 &#x1f388;线程池的拒绝策略 四.自己实现一个线程池 一.什么是线程池 简单来说&#xff0c;线程池就好比一块鱼塘&#xff0c;鱼塘中的每条鱼就是一个线程…

SQL注入绕过正则及无列名注入

渗透测试 一、select\b[\s\S]*\bfrom正则二、科学计数法绕过三、过滤information四、无列名注入1、利用 join-using 注列名。2、无列名查询 五、报错注入7大常用函数1.ST_LatFromGeoHash()&#xff08;mysql>5.7.x&#xff09;payload 2.ST_LongFromGeoHash&#xff08;mysq…

针对这两个趋势,3.0全新新零售商业模式可以采取以下策略:

国内市场确实存在“消费升级”和“消费降级”两个趋势&#xff0c;这是由于不同消费者群体的需求和购买力存在差异。消费升级主要发生在高端市场&#xff0c;消费者愿意为高品质、高价值、高价格的商品和服务付出更多。而消费降级则主要发生在中低端市场&#xff0c;消费者更加…

学习k8s

学习k8s 我为什么要用k8s 和其他部署方式的区别是什么? 传统部署方式 java --> package --> 放到服务器上 --> Tomcat 如果是同时进行写操作,会存在并发问题. 用户 --网络带宽–> 服务器 -->服务 同一个服务器上,多个服务: 网络资源的占用 内存的占用 cpu的占…

《点云处理》 点云去噪

前言 通常从传感器&#xff08;3D相机、雷达&#xff09;中获取到的点云存在噪点&#xff08;杂点、离群点、孤岛点等各种叫法&#xff09;。噪点产生的原因有不同&#xff0c;可能是扫描到了不想要扫描的物体&#xff0c;可能是待测工件表面反光形成的&#xff0c;也可能是相…

c# OpenCV 基本绘画(直线、椭圆、矩形、圆、多边形、文本)(四)

我们将在这里演示如何使用几何形状和文本注释图像。 Cv2.Line() 绘制直线 Cv2.Ellipse() 绘制椭圆Cv2.Rectangle() 绘制矩形Cv2.Circle() 绘制圆Cv2.FillPoly() 绘制多边形Cv2.PutText() 绘制文本 一、绘制直线 Cv2.Line(image, start_point, end_point, color, thickness) …

IntelliJ IDE 插件开发 | (三)消息通知与事件监听

系列文章 IntelliJ IDE 插件开发 |&#xff08;一&#xff09;快速入门IntelliJ IDE 插件开发 |&#xff08;二&#xff09;UI 界面与数据持久化IntelliJ IDE 插件开发 |&#xff08;三&#xff09;消息通知与事件监听 前言 在前两篇文章中讲解了关于插件开发的基础知识&…

C语言:前缀和

【模板】前缀和_牛客题霸_牛客网 (nowcoder.com) 前缀和&#xff1a;将数组小于该前缀的数加起来的新数组。 例&#xff1a; 该数组为1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5。前缀和为1&#xff0c;3&#xff0c;6&#xff0c;10&#xff0c;15 。 如果要求…

1-完全理解以太坊智能合约

了解区块链 区块链技术的核心概念是分布式账本&#xff0c;它是许多参与者共享的特定类型的数据库。 这个特殊的数据库只是一个交易列表&#xff0c;记录着网络中发生的每笔交易。每个人都可以拥有自己的交易列表备份&#xff0c;再加上强有力的货币激励措施消除各方之间信任…

比 style gan 更好的 style gan2

上一篇博客介绍了style gan 原理&#xff0c;但是 style gan 的结果会有水珠伪影&#xff0c;作者实验后发现是 Adain 导致的&#xff0c;AdaIN对每一个feature map的通道进行归一化&#xff0c;这样可能破坏掉feature之间的信息。当然实验证明发现&#xff0c;去除AdaIN的归一…

广州华锐互动VRAR:利用VR开展新能源汽车触电安全演练,降低培训成本和风险

随着新能源汽车行业的快速发展&#xff0c;相关的安全培训也变得越来越重要。其中&#xff0c;触电急救培训对于保障驾驶员和乘客的安全具有重要意义。传统培训方式存在一些不足&#xff0c;而利用VR技术进行培训则具有很多优势。 利用VR技术开展新能源汽车触电安全演练可以在模…