一文吃透KMP算法

news2025/1/20 18:24:18

       前言:今天,我们要来学习的字符串的高效匹配算法,KMP算法,用于在一个文本串中查找一个模式串的出现位置。相比于朴素的字符串匹配算法,KMP算法具有更低的时间复杂度,KMP算法的核心思想是利用已匹配的部分信息,避免重复比较已经比较过的字符。它通过构建一个模式串的前缀表(或称为部分匹配表),在匹配过程中根据前缀表的内容来决定下一次匹配的位置,下面,就让我们开始今天的学习之旅吧!

目录

1.暴力算法的启发

2.next数组引入

3.kmp算法

next数组如何求出?

kmp算法实现

完整代码实现


 

1.暴力算法的启发

     我们所熟知的暴力的匹配算法,其实也叫作BF算法,就是两个循环嵌套着来一点一点的挪动模式串,当某个位置模式串与被匹配的字符串完全相同时,就找到了匹配的位置,这里的就不再赘述,直接给出代码:

int bf(const string &s, const string &t) {
    int i = 0, j = 0;
    while (i < s.size() && j < t.size()) {
        if (s[i] == t[j]) i++, j++;
        else i = i - j + 1, j = 0;
    }
    if (j == t.size()) return i - j;
    else return -1;
}

动画演示: 

       看到上面的动画,我们不难从中看出主串和模式串进行比较的时候,主串的指针会一直震荡着向前移动,向前走发现不匹配后又要倒回来接着再和子串匹配,我们的kmp算法相较于传统的暴力算法的优势就是在于kmp算法的主串的指针只前进不后退,那这又是如何做到的呢?

      我们不妨假设在i处主串与模式串最后一次匹配成功,再往下就不能匹配了,那么,接下来按暴力的做法,就是将模式串右移一位再次从头进行比较,但是,当我们主串和模式串遇到第一个不匹配元素前,我们已经匹配成功了一些元素,这些元素中隐含着一些信息可以帮助我们优化后序的匹配过程,我们假设子串可以一次向右移动j个单位才能再次和主串匹配成功,那么,在模式串中,就会出现一些可以脱离主串而存在的关系:

2.next数组引入

首先,我们需要知道一些字符串的前后缀的知识:

      \cdot对于我们下面探讨度前后缀,以及公共前后缀,都是真前缀和真后缀,也就是不包含整个字符串的前后缀,比如\displaystyle对于字符串“ababc”,其真后缀仅包含“c”、“bc”、“abc”、“babc”、其真前缀仅包含“a”、“ab”、“aba”、“abab”,该字符串就没有公共的真前后缀。

        从上面的图中我们可以看出,对于模式串的任意一个位置,都可以求出一个使得该处模式串因为后序位置上的字符不匹配时的最小移动距离,这个移动距离其实是我们从上一次匹配中获得的主串和模式串之间的匹配关系信息,使我们知道从上一轮已经匹配成功的字符串中至少移动多少位才有可能有再次与主串匹配的可能,在暴力做法中,我们并没有利用这些上一轮比较后产生的信息,只是让模式串一位一位的向右移动,这就是我们kmp算法的核心,也是优化的核心。

       在比较的过程中,我们让模式串每个位置上的next值等于以该位置结尾的模式串的最长前后缀的长度,这样,我们一开始,仅仅利用模式串就能够求出对应的next数组,但是这和模式串的移动有什么关系呢?

下面,我们以模式串为“abababaab”为例,来模拟演示next数组的形成和模式串移动的关系

我们上面得出了一个结论,

模式串的移动距离=(当前不匹配的下标-1) - next[当前不匹配的下标-1];

这结论是否具有普遍性呢?下面我们来证明一下:

      我们不妨假设在主串的下标 i 处,在模式串的下标 j+1 处,发生了第一处字符不匹配,那么此时,模式串的最后一个与主串匹配处就在下标 j 处,我们利用前面的公式,可以得出,模式串需要右移 j - next[ j ]位,而我们知道,在第一次发生不匹配前,也就是模式串的[ 1 , j ]和主串的从[ i-j , i-1 ]已经是匹配的,根据我们的next数组可知,next[ j ]为从[ 1 , j ]的模式串子串的最大公共前后缀长度,因为前后缀一个在尾部,一个在头部,如果想要再次匹配主串某个位置和模式串的字符,我们至少要将我们已知的公共的前缀部分移动到后缀部分,才能够接续主串与模式串的对应此时我们需要的移动距离就是从[1 ,  j]d的长度减去公共前后缀的长度,也就是 j  -next[ j ],到此,结论得证。

3.kmp算法

      有了next数组,我们的kmp算法算是有了关键的核心了,接下来,如何将这个核心正确安装呢,不要急,我们来慢慢分析,主要分为next数组如何求出以及如何利用next数组来实现kmp算法两部分。

next数组如何求出?

       我看网上大多都是直接让next[0]=-1的操作,本质上是将前缀表右移然后将next[0]处自行置为了-1,其实关于next数组的实现有三种方式,但是都大同小异,我们就以最好理解的,根据我们的前后缀直接求出来的next直接算就可以了,

       首先,易知next[0]=0,我们假设数组下标从0开始,第一个字符相当于前缀包括尾,后缀包括头,所以最长相等前后缀是0,初始时,我们设置两个指针,指针i表示后缀的尾部,指针j表示前缀的尾部,这之所以设置在尾部,是因为前后缀一定包含头部或尾部元素,这样,对于我们不匹配的前后缀,我们回退指针j就可以知道指针i处的最长的前缀的长度,也就相当于知道了next[i]的值,初始时,我们需要让j=0,且i=1,因为后缀最短需要从1开始,也就是字符串至少有两个元素才能产生前后缀,这里分为两种情况:

      \cdotp[i]!=p[j],就相当于当前的前缀(前缀区间是[0,j) )最后一个元素与后缀(后缀区间是 [ i-j ,i) )最后一个元素不再匹配,因为我们此时求的是ne[i],所以,i指针不能再移动,我们只能以前缀的长度来确定我们的ne[i],因为需要前后缀相等,而后缀的尾部元素就是i指向的字符,所以,我们需要通过不断改变前缀尾部元素,也就是j指向的位置来与后缀相匹配,这也叫作回退过程,那么如何回退呢?这就要用到我们的前面的匹配信息了,我们在求ne[i]时,其实,我们已经知道了ne[i-1]和ne[j-1]的值,也就是上一轮的匹配结果,我们可以将当前的前缀回退或者缩小到上一轮比较的前缀,用这个的前缀的最后一个元素p[ne[j-1]]再次来和p[i]做比较,如果不匹配则j再次回退到上一个匹配前缀的尾部,直到匹配成功或者j回退到了0,退无可退,那么此时的ne[i]就直接等于0;

     \cdotp[i]==p[j],说明此时的区间[0,j]和区间[i-j,i]是完全相同的两个字符串,也就是此时前缀和后缀相等,此时,我们的最长相等前后缀就是j+1的长度,所以我们需要让j++,并且给ne[i]赋值即可,最后再让i++,继续新一轮的匹配。

求next数组的代码:

 //求next数组
    ne[0]=0;
    for(int i=1,j=0;i<p.size();i++)
    {
        while(j>0&&p[i]!=p[j])
         j=ne[j];
        if(p[i]==p[j])
         j++;
        ne[i]=j;
    }

kmp算法实现

      每次比较时,若 s [ i ]  与 t [ j ] 相等,则执行 i++, j++ 并继续比较;若不等,则执行 j = nxt[j] 以更新 j (相当于向右移动模式串)。一个特殊情况是,如果某一次比较出现了 s [ i ] 不等于 t [ 0 ] t[0],即在模式串的第一个字符处失配,则 j  的更新值为 j = ne[0] = -1,接下来应当执行 i++, j++,相当于让模式串右移一位并重新在模式串的第一个字符处进行比较。由此可见,定义 n e x t [ 0 ] = − 1 的作用是将两种情况合并为一种情况并达到简化代码的目的。我们通过让j初始直接赋值为0,对于第一个字符就不匹配的情况,我们可以直接移动主串的指针,从而达到将模式串整体向右移动一位的目的即可,代码实现如下:

//KMP匹配过程
    for(int i=0,j=0;i<s.size();i++)
    {
        while(j>0&&s[i]!=p[j])
          j=ne[j-1];
        if(s[i]==p[j])//如果两者匹配,那么i++,j++,i在for循环中自动加1,所以这里不在加
         j++;
        if(j==n)//如果前缀和模式串相同了,说明找到匹配的了
        {
            cout<<i-n+1<<" ";//输出开始匹配的第一个字符的下标
        }
    }

完整代码实现

#include<iostream>

using namespace std;

const int N=100010,M=1000010;

char p[N],s[M];//分别表示模式串和被比较的字符串
int ne[N];//next数组
int n,m;//n表示模式串长度,m表示主串长度
int main()
{
    cin>>n>>p>>m>>s;
    
    //求next数组
    ne[0]=0;
    for(int i=1,j=0;i<n;i++)
    {
        while(j>0&&p[i]!=p[j])
         j=ne[j-1];
        if(p[i]==p[j])
         j++;
        ne[i]=j;
    }
    // for(int i=0;i<n;i++)
    //   cout<<ne[i]<<" ";
    // cout<<endl;
    //KMP匹配过程
    for(int i=0,j=0;i<s.size();i++)
    {
        while(j>0&&s[i]!=p[j])
          j=ne[j-1];
        if(s[i]==p[j])//如果两者匹配,那么i++,j++,i在for循环中自动加1,所以这里不在加
         j++;
        if(j==n)//如果前缀和模式串相同了,说明找到匹配的了
        {
            cout<<i-n+1<<" ";//输出开始匹配的第一个字符的下标
        }
    }
    
    return 0;
}

    

       我从未见过一个早起、勤奋、谨慎、诚实的人抱怨命运不好的。你可以拒绝学习的动力,但是你的竞争者不会。勤奋、重复和大量的练习是给每一个普通人成才的机会,最完美的状态,不是你从不失误,而是你没有放弃成长。没人能把你变得越来越好,时间和精力只是陪衬,支撑你变得越来越好的是你坚强的意志、修行、品行,以及不断的反思和修正。人生最好的贵人,就是努力向上的自己。


 

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

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

相关文章

如何在Spring Boot应用中使用Nacos实现动态更新数据源

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

zabbix概述及简单的在centos7安装Zabbix5.0及添加监控对象

目录 一、概述 1、概念 2、Zabbix构成&#xff1a; 3、Zabbix的专业术语包括&#xff1a; 4、Zabbix的工作流程如下&#xff1a; 5、Zabbix的进程包括&#xff1a; 6、Zabbix的监控框架包括&#xff1a; 7、Zabbix的配置流&#xff1a; 8、zabbix程序结构 9、 zabbix…

基于YOLOV8模型的农作机器和行人目标检测系统(PyTorch+Pyside6+YOLOv8模型)

摘要&#xff1a;基于YOLOV8模型的农作机器和行人目标检测系统可用于日常生活中检测与定位农作机和行人目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的目标检测&#xff0c;另外本系统还支持图片、视频等格式的结果可视化与结果导出。本系统采用YOLOv8目标…

CXL.cache H2D/D2H 请求响应对应关系

&#x1f525;点击查看精选 CXL 系列文章&#x1f525; &#x1f525;点击进入【芯片设计验证】社区&#xff0c;查看更多精彩内容&#x1f525; &#x1f4e2; 声明&#xff1a; &#x1f96d; 作者主页&#xff1a;【MangoPapa的CSDN主页】。⚠️ 本文首发于CSDN&#xff0c…

【大数据】Apache Iceberg 概述和源代码的构建

Apache Iceberg 概述和源代码的构建 1.数据湖的解决方案 - Iceberg1.1 Iceberg 是什么1.2 Iceberg 的 Table Format 介绍1.3 Iceberg 的核心思想1.4 Iceberg 的元数据管理1.5 Iceberg 的重要特性1.5.1 丰富的计算引擎1.5.2 灵活的文件组织形式1.5.3 优化数据入湖流程1.5.4 增量…

手写RPC框架--3.搭建服务注册与发现目录结构

RPC框架-Gitee代码(麻烦点个Starred, 支持一下吧) RPC框架-GitHub代码(麻烦点个Starred, 支持一下吧) 搭建服务注册与发现目录结构 搭建服务注册与发现目录结构a.基于ZooKeeper的服务发现b.搭建基础工程c.基础代码d.编写架子工程e.创建zookeeper基础目录结构 搭建服务注册与发现…

ISP——3A算法

目录 前沿一. 自动曝光AE1.1. 自动曝光1.2. 18%灰1.3. 测光区域1.4. 摄影曝光加法系统1.5. AE算法1.5.1. 考虑事项1.5.2. AE实现过程 1.6. AE算法 二. 自动对焦AF2.1. 什么是自动对焦2.2. 图像清晰度评价方法2.2.1. Brenner 梯度函数2.2.2. Tenengrad 梯度函数2.2.3. Laplacian…

设计模式-8--模板方法模式(Template Method Pattern)

一、什么是模板方法模式&#xff08;Template Method Pattern&#xff09; 模板方法模式&#xff08;Template Method Pattern&#xff09;是一种行为型设计模式&#xff0c;它定义了一个算法的骨架&#xff0c;将一些步骤的实现延迟到子类中。模板方法模式允许在不改变算法的…

多机单目标跟踪Cross-Drone Transformer Network for Robust Single Object Tracking

1. 摘要 无人机已被广泛用于各种应用&#xff0c;如空中摄影和军事安全&#xff0c;因为与固定摄像机相比&#xff0c;无人机具有高机动性和广阔的视野。多架无人机跟踪系统可以通过收集不同视角的互补视频片段来提供丰富的目标信息&#xff0c;特别是当目标在某些视角下被遮挡…

微信小程序分包-主包尺寸 (不包合插件) 应小于 1.5 M

目录 起因 分包 最后 起因 更新一个之前的小程序&#xff0c; 上传的时候提示主包尺寸 (不包合插件) 应小于 1.5 M 怎么办&#xff1f;查看教程啊 开发者可通过开发者工具中的性能扫描工具提前发现代码中的可优化项&#xff1a; 1. 代码包不包含插件大小超过 1.5 M 【建议】…

1777_树莓派截图功能实现

全部学习汇总&#xff1a; GitHub - GreyZhang/little_bits_of_raspberry_pi: my hacking trip about raspberry pi. 最近入手了树莓派的3B版本&#xff0c;安装了官方推荐的Debian版本。之前使用Linux的几个发行版本的时候&#xff0c;系统中通常会有KDE等集成的截图工具&…

第一个实例:QT实现汽车电子仪表盘

1.实现效果 1.1.视频演示 QT 实现汽车仪表盘 1.2.实现效果截图 2.生成的安装程序 此程序是个windows下的安装程序,可以直接安装,看到汽车仪表盘的实现效果,安装程序从下面链接下载: 【免费】使用QT实现的汽车电子仪表盘,在windows下的安装程序资源-CSDN文库 3.功能概述…

go语言基础操作--二

a : 10str : "mike"//匿名函数&#xff0c;没有函数名字 形成一个闭包,函数定义&#xff0c;还没有调用f1 : func() { //:自动推到类型fmt.Println("a ", a)fmt.Println("str ", str)}f1()//给一个函数类型起别名 这个写法不推荐type FuncType …

存储成本降低85%,携程历史库场景的降本实践

携程&#xff0c;一家中国领先的在线票务服务公司&#xff0c;从 1999 年创立至今&#xff0c;数据库系统历经三次替换。在移动互联网时代&#xff0c;面对云计算卷积而来的海量数据&#xff0c;携程通过新的数据库方案实现存储成本降低 85% 左右&#xff0c;性能提升数倍。本文…

TensorRT来加速YOLO v5推理与检测

TensorRT来加速YOLO v5推理与检测 文章目录 TensorRT来加速YOLO v5推理与检测TensorRT简介一、TensorRT安装1. 电脑基础环境2. 查看 cuda 版本3. 安装TensorRT4. 验证TensorRT 二、YOLO v5模型导出函数1.onnx 模型导出&#xff08;def export_onnx&#xff08;&#xff09;函数…

nmon性能监控工具介绍【杭州多测师_王sir】

nmon监控工具 (nmon监控centos6X)1) 工具准备nmon16X(性能监控)和nmon_analyser(性能报告分析器)&#xff1b;2) 下载nmon页面地址:http://nmon.sourceforge.net/pmwiki.php?nSite.Download3) 下载指定版本nmon到centos6X:wget https://nchc.dl.sourceforge.net/project/nmon…

HTTP协议初识·中篇

加上目录&#xff0c;会出现导向不正确的情况&#xff0c;可能是bug&#xff0c;目录一长就容易出错&#xff1f; 本篇主要讲解了&#xff1a; 网页分离(网页代码和.c文件分离) html链接跳转 网页添加图片 确认并返回资源类型 填写正文长度属性 添加表单 临时重定向 补充知识&a…

04. 函数和函数调用机制

1. 先学习/复习C语言的入门知识 1.1 C语言简介 C语言是一种通用的编程语言&#xff0c;于1972年由丹尼斯里奇&#xff08;Dennis Ritchie&#xff09;创建。C语言最初目的是为了开发UNIX操作系统&#xff0c;但由于其简洁的语法、快速的执行速度和可移植性&#xff0c;自此成…

Linux系统中驱动入门设备树DTS(经典)

设备树&#xff08;DTS:device tree source&#xff09;&#xff0c;字面意思就是一块电路板上设备如上图中CPU、DDR、I2C、GPIO、SPI等&#xff0c;按照树形结构描绘成的一棵树。按照策略和功能分离的思路&#xff0c;就是驱动代码&#xff08;功能&#xff09;和设备树DTS配置…

算法通关村16关 | 滑动窗口如此简单

1. 滑动窗口基本思想 滑动窗口就是快慢指针问题&#xff0c;快指针走一步&#xff0c;慢指针走一步&#xff0c;维护窗口的大小。 下图有大小为3的滑动窗口移动示例。 窗口&#xff1a;窗口是left和right之间的元素&#xff0c;也可以理解为一个区间。窗口的大小可能固定&#…