「字符串」前缀函数|KMP匹配:规范化next数组 / LeetCode 28(C++)

目录

概述

思路 

核心概念:前缀函数

1.前缀函数

2.next数组

1.考研版本

2.竞赛版本

 算法过程

构建next数组

匹配过程

复杂度 

Code


概述

为什么大家总觉得KMP难?难的根本就不是这个算法本身。

在互联网上你可以见到八十种KMP算法的next数组定义和模式串回滚策略,把一切都懂得特别混乱。很多时候初学者的难点根本不在于这个算法本身,而是它令人痛苦的百花齐放的定义。

有的next数组从0下标开始,有的从1开始;有的表示不包括本字符的前面部分的真前后缀,有的表示包括本字符的的前后缀,有的回滚+1,有的不+1,而他们却总是忽略这些异同,自顾自地讲KMP的匹配问题。初学者看到这直接傻了眼:随便挑两个视频或者文章,他们的定义和递推手段都不一样,让理解难度雪上加霜。

下面我们来先从字符串匹配讲起,想一想什么样的next数组定义才最适合这个算法本身。

LeetCode 28:

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回  -1 

示例 :

输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。

*注意*: 接下来我们将称呼haystack为主串,needle为模式串。


思路 

最普通的暴力算法总是在匹配失败后将主串指针i回滚到开始匹配的位置,模式串指针j回滚到0,然后在一轮for循环结束后执行i++操作来跳过这个匹配失败的位置。来看看Code。

class Solution {
public:int strStr(string haystack, string needle) {const int n=haystack.size();const int m=needle.size();for(int i=0;i<n;i++){if(haystack[i]==needle[0])for(int k=i,j=0;k<n;k++){if(haystack[k]==needle[j])j++;else break;if(j==m)return i;}}return -1;}
};

来想一想:都回滚j了,为什么还回滚i?难道就因为有一个字符匹配失败了就放弃前面所有的努力吗?

至少在最后的失败字符之前,我们匹配成功了。这意味着:

模式串为pattern,主串为mainj
pattern[j]  a  b  c  a  b  f  g√  √  √  √  √  ×main[i]  a  b  c  a  b  k  v  q  x  ti

至少在'f'字符与'k'字符对比之前,我们的匹配成功了。

这意味着,main函数的此前部分和pattern的此前部分一一对应,我们称之为str1。

这时候的一个独到之处就是:

在这段主串和模式串共同的[开头,失配位置)的子串str1中,匹配失败之前的任意位置,从这个位置开始,一直到匹配失败的位置之前他们都相同,这部分[任意位置,失配位置)的子串我们称为str2

那我们可以有一个特殊的回滚模式串指针j的手段:不回滚到最开头,而是回滚到模式串的某个位置,在这个位置以前的部分模式串[开头,某位置)称为str3,它与str2相同。

结合例子感受一下:

模式串为pattern,主串为main┌str3┐ j<-------j
pattern[j] |a  b  c  a  b |f  g|        └str2┘| |√  √  √  √  √ ||        ┌str2┑|main[i] |a  b  c  a  b |k  v  q  x  t└----str1------┘i

这样看还是不够清晰,我们把j的移动形象理解成模式串的移动。

模式串为pattern,主串为main┌str3┐ j<-------j
pattern[j]           a  b  c  a  b  f  g└str2┘ √  √┌str2┑main[i]  a  b  c  a  b  k  v  q  x  t└----str1-----┘ i

也就是说:在j倒退回某个位置后,这位置之前的模式串部分和主串部分是天生匹配的。

接下来我会用前后缀的语言代替“某某部分”:

//这是对前后缀的解释,如果你了解,可以跳过
对于一个字符串
begin                enda b a c d x f a c a d---->           ---->前缀             后缀    //*注意*:前后缀的字符顺序都是从前向后
前缀是从[begin,x]的任意子字符串 真前缀的x不等于end
后缀是从 [x,end] 的任意子字符串 真后缀的x不等于begin

归纳一下

当匹配到主串i位置和模式串j位置,匹配失败时:

str2既是主串的[0,i)子串的一个后缀,又是模式串的[0,j)子字符串的一个后缀。

str3则是模式串中[0,j)子串的某个前缀,这个前缀与str2这个后缀相等。

因此,模式串中的str3能与模式串中的str2匹配,就意味着模式串中的str3与主串中的str2与是天生匹配的。

形象理解:

           str3==str2
pattern -str3------str2-√√√√√√√√√√√√√√√×
main    -----------str2-↓
pattern           -str3------str2-√√√√√?
main    -----------str2-

那按理来说,这是一个模式串的自匹配问题:对于模式串的每个位置j,都有[0,j)子串,都要找到他们的最长相同真前后缀。

因此问题就转化成了:

枚举模式串的每个下标j,求成它的[0,j)子字符串的最长相同真前后缀,这样,当在任意位置匹配失败时,都可以知道j的回滚位置了,而i从不回滚。


核心概念:前缀函数

网络上各种奇异搞笑的next数组定义的根本来源是他们没搞懂前缀函数和next数组的区别。

前缀函数是一个独立的算法函数概念,next数组只是它针对于KMP算法的特化版本。

1.前缀函数

它写作π[i]或PM[i]

定义:

给定一个长度为n的字符串,其前缀函数被定义为一个长度为n的数组π[n](或:PM[n])。 其中π[i]的意义是字符串的[0,i]子字符串的最长相同真前后缀

故有:

           j  0 1 2 3 4 5 6
string str[j] a b c a b c dπ[j] 0 0 0 1 2 3 0

这是标准的前缀函数定义,他长度就是n,下标起始位置就是0,其他的任何一种next数组都是他的特化版本。

*注意*:我通常使用j来作为next数组和模式串的索引。

2.next数组

在KMP匹配算法中,π数组变成了next数组,有如下几种方案:

1.考研版本

①模式串、主串和next数组下标统一从1开始计数。[j]表示第j个字符处,而不是索引0代表第1个字符。next[0]值为-1。

②next[j]表示原字符串{s[1]...s[j-1]}的最长相同真前后缀长度,它记录在了next[j]。next[j]的值不包括第j个字符。

③回滚代码:j=next[j]+1。

例如上文的"abcabcd",如果当j=7失配时,next[j]==3,那么j会从4开始继续匹配,跳过了123。

           j  0  1  2  3  4  5  6  7
string str[j] n  a  b  c  a  b  c  dnext[j] -1 0  0  0  0  1  2  3  
//n通常储存字符串的长度信息。

*注意*:你还会见到next[0]=0,且next数组整体+1的版本,它是另一个考研版本,只是将回滚代码的+1操作融入了next数组中,回滚代码:j=next[j],此处不再赘述。 

2.竞赛版本

事实上,在竞赛或者各大算法平台,字符串下标仍然从0开始,我们要为这个原则服务。

①字符串下标仍然从0开始,但我们仍然期望next数组下标从1开始。即主串与模式串从0开始,next数组从1开始,next数组长度比模式串大1,后续你会发现这样做的好处。

②next[j]表示原字符串*前j个字符*的最长相同真前后缀长度,它记录在了next[j],这里发生了错位。(next[0]=0,这样当j=0时执行回滚不会发生溢出,next[1]=0,第一个字符没有真先后缀)。

③回滚代码:j=next[j]。next数组错位的目的就是避免回滚代码发生+1-1的问题,这样能有效规避溢出。

例如上文的"abcabcd" ,如果当j=6失配时,next[j]==3,那么j会从3开始继续匹配,跳过了012。

即:j在某处失配时,它前面有j个字符,且这j个字符最长相同前后缀长度len储存在了next[j],可以快速访问next[j]得到j的回滚位置,回滚后恰好跳过len个字符。(我们总是期望在一轮for循环后j指向一个有待下一轮循环商榷的位置,不论是j++还是j=next[j],都是这样的。)

           j  0  1  2  3  4  5  6  7
string str[j] a  b  c  a  b  c  d \0next[j]    0  0  0  1  2  3  0  

 算法过程

*注意*:我们会以竞赛版本的KMP进行讲解。它稍微更改就可以变成考研版。(比如insert函数)

构建next数组

构建next数组才是KMP本身具有难度的地方。

推论:字符串增加一个字符,它的最长相同真前后缀至多+1。这个不起眼的推论是构建的核心。

但是我们可以将其总结为3点:

①next[0]=0,next[1]=0,这两个直接无视。随后发生for循环int i=1,j=0。

i向前探索;j即用于为next[i+1]赋值,又作为下标索引与向前探索的i遥相呼应:[0,j]与[i-j,i]匹配。

因此j同时代表着:[0,i]的公共前后缀长度数值,也是与i这个前方洗标呼应的后方下标。

记得我们的定义是:next[j]表示原字符串*前j个字符*的最长相同真前后缀长度,i+1这个值才是[0,i]字符串的字符个数,因此j为next[i+1]赋值。

②当pattern[i]!=pattern[j],即位置i与位置j字符不匹配,通过while循环回滚j,如果仍失配,继续回滚,一直到j==0。

这一点是KMP的精髓所在:我们一边构建next数组,一边利用next数组回滚j。

这听起来很不可思议,但是注意:next数组是从前向后构建的,而回滚是向前的。这说明:我们在利用已经构建起的next数组进行回滚,而不会发生某种奇怪的冲突。

但是为什么用next数组回滚j呢?还记得next数组是干什么用的吗?它就是指示:当失配时,请从这里再试一试。不一定非要模式串与主串匹配才有失配,模式串自匹配时前缀不等于后缀也叫失配。我们回滚j就是期望将j回滚到可能与pattern[i]匹配的位置。

如果一直到j==0还失败,那就意味着不存在相同前后缀,那么为next[i+1]=j(即赋0值也是合理的了)

③判断pattern[i]是否等于pattern[j]。

如果因为j与i成功匹配而脱离while循环,那么j++,因为我们的最长相同前后缀+1。

void get_next(string&pattern,vector<int>&next){next[0]=0,next[1]=0;const int m=pattern.size();for(int i=1,j=0;i<m;i++){while(j&&pattern[j]!=pattern[i])j=next[j];if(pattern[j]==pattern[i])j++;next[i+1]=j;//赋值发生在j++之后,所以此处不用+1}}

匹配过程

匹配过程与构建过程极其类似。

主要是以下三点:

①当main[i]!=pattern[j],即在此处失配时,通过while循环回滚j,如果仍失配,继续回滚,一直到j==0。

跳出while后判断main[i]是否等于pattern[j]。

由于脱离while要么是两者相等要么不相等但j==0,那么:
if为真意味着:两者匹配成功,j++,i++。(两者在一轮循环后分别指向有待下一轮循环商榷的位置。)

if为假意味着:这一步就是判断出现j等于0且仍无法匹配的状况,那么认为这个i终究无法匹配,j停滞在0,随后放弃i的当前位置,i自增。

③判断j==m,这意味着完全匹配,返回i-m+1。(注意这里有个+1的细节:一轮for循环结束之前i++还未发生)

for(int i=0,j=0;i<n;i++){while(j&&main[i]!=pattern[j])j=next[j];if(main[i]==pattern[j])j++;if(j==m)return i-m+1;
}
return -1;

复杂度 

时间复杂度:O(n+m)

空间复杂度:O(m)

n:主串长度

m:模式串长度


Code

class Solution {
public:void get_next(string&pattern,vector<int>&next){const int m=pattern.size();for(int i=1,j=0;i<m;i++){while(j&&pattern[j]!=pattern[i])j=next[j];if(pattern[j]==pattern[i])j++;next[i+1]=j;}}int strStr(string haystack, string needle) {const int n=haystack.size(),m=needle.size();vector<int>next(m+1,0);get_next(needle,next);for(int i=0,j=0;i<n;i++){while(j&&haystack[i]!=needle[j])j=next[j];if(haystack[i]==needle[j])j++;if(j==m)return i-m+1;}return -1;}
};

(如果你充分理解了本文,就会发现代码竟然如此直观) 。

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

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

相关文章

项目1 物流仓库管理系统

一、项目概述 本项目旨在开发一个功能全面的物流仓库管理系统&#xff0c;以数字化手段优化仓库作业流程&#xff0c;提高管理效率。系统集成了前端用户交互界面与后端数据处理逻辑&#xff0c;涵盖了从用户注册登录、订单管理、货单跟踪到用户信息维护等多个核心业务模块。通…

基于django的学生作业提交与管理系统,有管理后台,可作为课设使用

在本项目中&#xff0c;我们设计并实现了一个基于Django框架的学生作业提交与管理系统&#xff0c;旨在为教师和学生提供一个高效、便捷的作业管理平台。Django作为一个高效的Web框架&#xff0c;因其强大的功能和灵活的架构&#xff0c;使得本系统能够快速开发并扩展。 系统功…

Maven的简单使用

Maven使用 Maven的作用1. 自动构建标准化的java项目结构(1) 项目结构① 约定目录结构的意义② 约定大于配置 (2)项目创建坐标坐标的命名方法&#xff08;约定&#xff09; 2. 帮助管理java中jar包的依赖(1) 配置使用依赖引入属性配置 (2) maven指令(3) 依赖的范围(4) 依赖传递(…

【密码学】密钥管理:②密钥分配

一、密钥分配的定义 密钥分配是密钥管理生命周期中最重要的部分&#xff0c;密钥分配方案研究的是密码系统中密钥的分发和传送问题。从本质上讲&#xff0c;密钥分配为通信双方建立用于信息加密、解密签名等操作的密钥&#xff0c;以实现保密通信或认证签名等。 &#xff08;1…

win10蓝牙只能发送,无法接收

给win10升了级&#xff0c;到22H2&#xff0c;蓝牙出了问题 以前接收&#xff0c;就是默认直接就可以接收。现在只能发送&#xff0c;无法接收。 在网上找了很多办法都没奏效&#xff0c;目前的方法是&#xff0c; 每次接收&#xff0c;都要操作一次&#xff0c;而不是自动接…

leetcode-538. 把二叉搜索树转换为累加树

题目描述 给出二叉 搜索 树的根节点&#xff0c;该树的节点值各不相同&#xff0c;请你将其转换为累加树&#xff08;Greater Sum Tree&#xff09;&#xff0c;使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。 提醒一下&#xff0c;二叉搜索树满足下列约束…

计量自动化终端上行通信规约

物理层 TCP 和 UDP 的传输接口 该类接口的登录链接和心跳检测采用链路测试服务&#xff0c;链路测试周期可设定。 参见 TCP/IP 协议规范。 串行通信传输接口 字节传输按异步方式进行&#xff0c;它包含 8 个数据位、1 个起始位“0”、1 个偶校验位 P 和 1 个停止位“1”。 …

Android Studio 动态表格显示效果

最终效果 一、先定义明细的样式 table_row.xml <?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_h…

集团数字化转型方案(四)

集团数字化转型方案通过全面部署人工智能&#xff08;AI&#xff09;、大数据分析、云计算和物联网&#xff08;IoT&#xff09;技术&#xff0c;创建了一个智能化的企业运营平台&#xff0c;涵盖从业务流程自动化、实时数据监控、精准决策支持&#xff0c;到个性化客户服务和高…

实验七:独立按键实验

硬件电路图和题目; LED1-LD8是 P2口8个管脚 mian.c #include<reg52.h>sbit But1=P3^1 ; sbit But2=P3^0 ; sbit But3=P3^2 ; sbit But4=P3^3 ;sbit LED1 =P2^0 ; sbit LED2 =P2^1 ; sbit LED3 =P2^2 ; sbit LED4 =P2^3 ;#define PRESS_1 1 #define PRESS_…

CUTLASS 中的 47_ampere_gemm_universal_streamk 示例

前一篇文章介绍了 Stream-K: Work-centric Parallel Decomposition for Dense Matrix-Matrix Multiplication on the GPU 论文&#xff0c;下面对其代码实现进行分析。 cutlass 的 examples/47_ampere_gemm_universal_streamk 展示了 GEMM Stream-K 算法在 Ampere 架构上的使用…

JNPF 5.0升级钜惠,感恩回馈永远在路上

尊敬的JNPF用户们&#xff1a; 经过引迈团队数月的辛勤努力和不断的技术创新&#xff0c;JNPF快速开发平台迎来全新升级——5.0版本&#xff01;此次5.0版本的迭代革新&#xff0c;不仅代表着我们技术实力的进一步提升&#xff0c;是我们对用户需求的深度理解和积极回应。为了…

基于C# winform部署图像动漫化AnimeGANv2部署onnx模型

【界面截图】 【效果演示】 【部分实现代码】 using System; using System.Diagnostics; using System.Windows.Forms; using OpenCvSharp;namespace FIRC {public partial class Form1 : Form{Mat src null;public Form1(){InitializeComponent();}private void button1_Cli…

html+css+js网页设计 天猫首页

htmlcssjs网页设计 天猫首页 网页作品代码简单&#xff0c;可使用任意HTML编辑软件&#xff08;如&#xff1a;Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作&#xff09;。 获取源码 1&#xff0c;访问…

git本地仓库同步到远程仓库

整个过程分为如下几步&#xff1a; 1、本地仓库的创建 2、远程仓库的创建 3、远程仓库添加key 4、同步本地仓库到远程仓库 1、本地仓库的创建&#xff1a; 使用如下代码创建本地仓库&#xff1a; echo "# test" >> README.md git init git add README.md …

用户增长:策略与实践,驱动SaaS企业持续繁荣

在当今这个数字化时代&#xff0c;用户增长已成为所有行业&#xff0c;尤其是SaaS&#xff08;Software as a Service&#xff0c;软件即服务&#xff09;企业生存与发展的核心驱动力。用户增长不仅关乎市场份额的扩大&#xff0c;更是企业价值实现和持续盈利的基石。那么&…

【计算机网络】网络版本计算器

此前我们关于TCP协议一直写的都是直接recv或者read&#xff0c;有了字节流的概念后&#xff0c;我们知道这样直接读可能会出错&#xff0c;所以我们如何进行分割完整报文&#xff1f;这就需要报头来解决了&#xff01; 但当前我们先不谈这个话题&#xff0c;先从头开始。 将会…

【秋招笔试】8.18大疆秋招(第一套)-后端岗

🍭 大家好这里是 春秋招笔试突围,一起备战大厂笔试 💻 ACM金牌团队🏅️ | 多次AK大厂笔试 | 编程一对一辅导 ✨ 本系列打算持续跟新 春秋招笔试题 👏 感谢大家的订阅➕ 和 喜欢💗 和 手里的小花花🌸 ✨ 笔试合集传送们 -> 🧷春秋招笔试合集 🍒 本专栏已收…

Springboot发邮件功能如何实现?详细步骤?

Springboot发邮件配置指南&#xff1f;如何集成Spring Mail发邮件&#xff1f; 无论是用户注册、密码重置还是重要通知的发送&#xff0c;邮件都是不可或缺的沟通方式。Springboot作为一个流行的Java开发框架&#xff0c;提供了简洁易用的方式来实现邮件功能。AokSend将详细探…

音频转换器有哪些?一键转换,轻松享受

暑假里&#xff0c;你是否也沉浸在激情四溢的演唱会中&#xff0c;用手机记录下了那些难忘的现场音频&#xff1f; 但回到家中&#xff0c;想要将这些珍贵的现场记忆从手机迁移到电脑上永久保存时&#xff0c;却遇到了格式不兼容的难题。 别担心&#xff0c;今天我们就要解决…