C++刷题 -- KMP算法

news2024/9/30 9:25:44

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里出现了模式串t
        return (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数组里记录的起始位置为-1
        for (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里出现了模式串t
                return (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++)
        {
            //判断当前两指针所指字符是否相等,不相等就回退j
            while(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.coloradmin.cn/o/1322778.html

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

相关文章

数据可视化---箱线图

类别内容导航机器学习机器学习算法应用场景与评价指标机器学习算法—分类机器学习算法—回归机器学习算法—聚类机器学习算法—异常检测机器学习算法—时间序列数据可视化数据可视化—折线图数据可视化—箱线图数据可视化—柱状图数据可视化—饼图、环形图、雷达图统计学检验箱…

【MySQL】Sql优化之索引的使用方式(145)

索引分类 1.单值索引 单的意思就是单列的值&#xff0c;比如说有一张数据库表&#xff0c;表内有三个字段&#xff0c;分别是 id name numberNo&#xff0c;我给name 这个字段加一个索引&#xff0c;这就是单值索引&#xff0c;因为只有name 这一列是索引&#xff1b; 一个表…

k8s-ingress特性 9

TLS加密 创建证书 测试访问 auth认证 创建认证文件 rewrite重定向 进入域名时&#xff0c;会自动重定向到hostname.html 示例&#xff1a; 测试 版本的升级迭代&#xff0c;之前利用控制器进行滚动更新&#xff0c;在升级过程中无法做到快速回滚 更加平滑的升级&#xff1…

猫头虎带您探索StringUtils3之StringUtils.isEmpty()和StringUtils.isBlank()的区别 ‍

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

低代码汹涌澎湃,真的是风口?

“中国的下一个风口是什么&#xff1f;低代码&#xff1f;你赞同吗&#xff1f;” 可以肯定的是&#xff0c;企业想站上未来产业的风口&#xff0c;”数字化“是必经之路。而低代码是企业向制高点突进的手段。产业的风口&#xff0c;就是产业发展的趋势&#xff0c;只有尽力站上…

SQL面试题挑战01:打折日期交叉问题

目录 问题&#xff1a;SQL解答&#xff1a;第一种方式&#xff1a;第二种方式&#xff1a; 问题&#xff1a; 如下为某平台的商品促销数据&#xff0c;字段含义分别为品牌名称、打折开始日期、打折结束日期&#xff0c;现在要计算每个品牌的打折销售天数&#xff08;注意其中的…

JNI 注册

一、 JNI 静态注册 ① 在 Android 的 Java 层定义方法 : 在 MainActivity 类中定义 如下 Native 方法 ; public native String stringFromJNI();② Native 方法实现 : 下面是一个 Native 方法实现的示例 ; extern "C" JNIEXPORT jstring JNICALL Java_kim_hsl_jni_…

VuePress安装及使用——使用 Markdown 创建你自己的博客网站和电子书

目录 前言一、依赖环境二、vuepress 安装和使用1.初始化2.将 VuePress 安装为本地依赖3. package.json 中添加脚本4. 新建 docs 文件夹5.启动6. 效果 三、进阶使用1.新增配置文件2.安装搜索插件3.config.js 中增加配置4.效果展示5.注意 四、使用主题1.安装2. 目录结构说明&…

fastjson反序列化 CVE-2017-18349

fastjson fastjson 是阿里巴巴开发的 java语言编写的高性能 JSON 库,用于将数据在 Json 和 Java Object之间相互转换。它没有用java的序列化机制,而是自定义了一套序列化机制。 提供两个主要接口&#xff1a; JSON.toJSONString 和 JSON.parseObject/JSON.parse 分别实现序列化…

双指针——找到字符串中的所有字母异位词

https://leetcode.cn/problems/find-all-anagrams-in-a-string/description/?envTypestudy-plan-v2&envIdtop-100-liked 双指针&#xff0c;每次都统计出来p长度的滑动窗口里的数字,拿Arrays.equals进行对比,然后滑动一小格&#xff0c;减1加1继续比对即可。 class Solut…

AngularJS

理解实现代码的逻辑为主要&#xff0c;代码怎么写为次要。 参考资料&#xff1a; 《AngularJS入门与进阶》&#xff0c;江荣波著 前端开发常用框架 React&#xff1a;由Facebook开发&#xff0c;用于构建用户界面的JavaScript库&#xff0c;以组件化和虚拟DOM著称。 Angular&…

Github项目推荐-30天Python教程

项目地址 GitHub - Asabeneh/30-Days-Of-Python 项目简述 这是一个非常棒的python入门教程&#xff0c;图文并茂&#xff0c;讲解清晰&#xff0c;十分值得推荐。 项目截图

在 Kubernetes 上部署 Python 3.7、Chrome 和 Chromedriver(版本 114.0.5735.90)的完整指南

一、构建基础镜像 docker build -f /u01/isi/DockerFile . -t thinking_code.com/xhh/crawler_base_image:v1.0.2docker push thinking_code.com/xhh/crawler_base_image:v1.0.2 二、K8s运行Pod 三、DockerFile文件 # 基于镜像基础 FROM python:3.7# 设置代码文件夹工作目录…

删除win10服务

之前工作需要在我电脑上安装了一个天气的服务&#xff0c;我想删掉但是在任务管理器里删不掉&#xff0c;请教了大佬 删除方法是&#xff0c;在开始菜单里找到命令提示符 右键-以管理员方式打开命令提示符 输入sc 按enter键&#xff0c;可以查看一些操作命令 删除服务的话&a…

利用虚继承解决菱形继承(钻石继承)的问题 学习笔记

菱形继承概念&#xff1a; 两个派生类继承同一个基类 又有某个类同时继承两个派生类 这种继承又被称为菱形继承&#xff0c;或者钻石继承 定义一个基类Animal&#xff0c;两个派生类Sheep、Camal&#xff0c;SheepCame继承于两个派生类 class Animal { public:int m_Age; }…

java-sec-code中命令注入

java-sec-code中命令注入 不安全的写法(由于我这里使用的是idea搭建的环境&#xff0c;原本命令执行的sh -c 无法运行&#xff0c;后续的输出也会产生乱码&#xff0c;修改部分代码&#xff0c;以便能更好的显示出效果&#xff0c;linux下使用docker木有问题) //需要导入的库文…

物流项目话术(1.5w字精选)

物流项目的分类 技术架构图&#xff08;面试时面试官会要求你画出技术架构图&#xff09; 功能结构图 业务功能流程 流程说明&#xff1a; 用户在【用户端】下单后&#xff0c;生成订单 系统会根据订单生成【取件任务】&#xff0c;快递员上门取件后成功后生成【运单】 用户对…

【leetcode203】移除链表元素【Java代码讲解】

12.18 移除链表元素 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5]示例 2&#xff…

算法设计与分析2023秋-头歌实验-实验七 动态规划

文章目录 第1关&#xff1a;数塔问题任务描述相关知识编程要求解题思路测试说明参考答案 第2关&#xff1a;最长公共子序列任务描述相关知识编程要求解题思路&#xff1a;测试说明参考答案 第3关&#xff1a;求序列-2 11 -4 13 -5 -2的最大子段和任务描述相关知识编程要求解题思…

单位委托保管档案好处在哪里

单位委托保管档案可以享受专业的管理、节省空间、提高效率、保护保密性和安全性&#xff0c;以及保证档案的长期保存等好处&#xff0c;从而提升工作效率和档案管理的质量。 具体而言&#xff0c;委托档案寄存专久智能保管档案的好处有以下几点&#xff1a; 1. 专业管理&#x…