灵神DAY3 KMP算法

 

具体解释:

1. 真前缀和真后缀的定义

前缀:字符串的起始部分。例如,字符串 s = "aabcaa" 的前缀是 """a""aa""aab""aabc""aabca"、"aabcaa"。 

 后缀:字符串的结尾部分。例如,字符串 s = "aabcaa" 的后缀是 """a""aa""caa""bcaa""abcaa""aabcaa"

真前缀:前缀中去掉整个字符串本身的部分。例如,字符串 s = "aabcaa" 的真前缀是 """a""aa""aab""aabc"、"aabca"。

真后缀:后缀中去掉整个字符串本身的部分。例如,字符串 s = "aabcaa" 的真后缀是 """a""aa""caa""bcaa""abcaa"

2.什么是 border(边界)?

border 的定义:是非空的真前缀和真后缀的交集。 

border 的例子
对于字符串 s = "aabcaa",我们来看其 border

  • "a":既是真前缀 "a",也是真后缀 "a"
  • "aa":既是真前缀 "aa",也是真后缀 "aa"

不属于 border 的部分

  • "aab" 是真前缀,但不是真后缀,因此不是 border
  • "caa" 是真后缀,但不是真前缀,因此也不是 border

3.什么是 π 数组? 

  • 对于字符串的每个前缀 p[:i]π[i] 表示前缀 p[:i]最长 border 的长度
π 数组的构造
  • 举例说明:对于模式串 p = "aabcaa"
    • p[:1] = "a":最长 border 长度是 0(无border)。:注意此时算的是真前缀和真后缀,a自己不算,因此没有border
    • p[:2] = "aa":最长 border 长度是 1"a"border)。
    • p[:3] = "aab":最长 border 长度是 0(无border)。
    • p[:4] = "aabc":最长 border 长度是 0(无border)。
    • p[:5] = "aabca":最长 border 长度是 1"a"border)。
    • p[:6] = "aabcaa":最长 border 长度是 2"aa"border)。
  • 结果:π 数组为 [0, 1, 0, 0, 1, 2]

4. 使用 π 数组快速匹配 

π 数组的作用是,当在主串中匹配失败时,利用模式串已有的部分匹配信息,快速跳过不必要的比较。

核心思想
  • 假设匹配到主串的某个位置时,模式串中某些前缀已经匹配了,此时若失配,可以直接跳过已匹配的部分,从下一个可能的匹配位置开始继续比较。

5. π 数组与 next 数组的关系

国内一些数据结构教材(如严蔚敏的)中,将 π 数组 定义为 next 数组,但存在以下差异:

  • next[i+1] = π[i] + 1
  • π 数组整体右移一位,所有元素值加一。

6. KMP 算法的匹配过程 

整体代码(构建 π 数组):

def calculateMaxMatchLengths(pattern):
    maxMatchLengths = [0] * len(pattern)  # 初始化 π 数组,长度与模式串相同
    maxLength = 0  # 当前前缀的最长 border 长度
    for i in range(1, len(pattern)):  # 从第二个字符开始遍历模式串
        # 当 pattern[maxLength] 与 pattern[i] 不匹配时,回退
        while maxLength > 0 and pattern[maxLength] != pattern[i]:
            maxLength = maxMatchLengths[maxLength - 1]  # 回退到之前的最长 border
        # 如果当前字符匹配,则扩展 border 长度
        if pattern[maxLength] == pattern[i]:
            maxLength += 1
        maxMatchLengths[i] = maxLength  # 记录当前前缀的最长 border 长度
    return maxMatchLengths

1.预处理 π 数组

  • 通过模式串计算 π 数组,记录模式串中每个前缀的最长 border 长度。

2.匹配主串

  • 利用 π 数组,在主串中寻找模式串的位置。
  • 如果匹配失败,根据 π 数组跳过不必要的比较。

构造 π 数组(前缀函数) 

1. 初始化 π 数组

 maxMatchLengths = [0] * len(pattern)

  • 初始化 π 数组(maxMatchLengths),其长度与模式串 pattern 相同。
  • π 数组的每个位置记录模式串前缀的最长 border 长度。
  • 例如:对于模式串 pattern = "aabcaa",π 数组初始化为 [0, 0, 0, 0, 0, 0]

2. 初始化 maxLength

maxLength = 0 
  • maxLength 变量表示当前前缀的最长 border 长度。
  • 初始为 0,因为第一个字符的前缀没有非空 border。
3. 遍历模式串

for i in range(1, len(pattern)):

  • 遍历模式串,从索引 1 开始,因为第一个字符没有非空 border(π[0] 固定为 0)。
4. 检查字符匹配并回退

while maxLength > 0 and pattern[maxLength] != pattern[i]: maxLength = maxMatchLengths[maxLength - 1]

  • 目的:当 pattern[maxLength]pattern[i] 不匹配时,根据 π 数组的值回退,寻找下一个可能的 border。
  • 逻辑
    • 如果当前最长 border 的下一个字符(pattern[maxLength])与当前字符(pattern[i])不匹配,则回退到上一个可能的 border,即 maxMatchLengths[maxLength - 1]
    • 为什么?
      • 当前的 border 不匹配时,可以通过 π 数组找到上一个可能的匹配位置,避免从头重新比较。
5. 扩展 border

if pattern[maxLength] == pattern[i]: maxLength += 1

  • 目的:如果当前字符匹配,说明 border 可以延长一位。
  • maxLength += 1 表示当前前缀的最长 border 长度增加。
6. 更新 π 数组

maxMatchLengths[i] = maxLength

  • 将当前前缀的最长 border 长度记录到 π 数组中。

运行示例

假设模式串为 pattern = "aabcaa",我们计算 π 数组:

1. 初始化

maxMatchLengths = [0, 0, 0, 0, 0, 0] maxLength = 0

2. 遍历计算
  • i = 1:

    • pattern[maxLength] = "a"pattern[1] = "a",匹配。
    • maxLength += 1maxLength = 1
    • maxMatchLengths[1] = 1

i = 2:

  • pattern[maxLength] = "a"pattern[2] = "b",不匹配。
  • 回退:maxLength = maxMatchLengths[maxLength - 1] = maxMatchLengths[0] = 0
  • maxLength = 0
  • maxMatchLengths[2] = 0

i = 3:

  • pattern[maxLength] = "a"pattern[3] = "c",不匹配。
  • maxLength = 0
  • maxMatchLengths[3] = 0

i = 4

  • pattern[maxLength] = pattern[0] = 'a'
  • pattern[i] = pattern[4] = 'a'
  • 匹配,maxLength += 1maxLength = 1
  • maxMatchLengths[4] = 1

i = 5

  • pattern[maxLength] = pattern[1] = 'a'
  • pattern[i] = pattern[5] = 'a'
  • 匹配,maxLength += 1maxLength = 2
  • maxMatchLengths[5] = 2

i = 6

  • pattern[maxLength] = pattern[2] = 'b'
  • pattern[i] = pattern[6] = 'b'
  • 匹配,maxLength += 1maxLength = 3
  • maxMatchLengths[6] = 3

函数 search 搜索

1. 初始化

positions = []
maxMatchLengths = calculateMaxMatchLengths(pattern)
count = 0

  • positions:存储模式串匹配的起始位置。
  • maxMatchLengths:调用 calculateMaxMatchLengths 函数计算 π 数组。
  • count:记录当前模式串匹配了多少字符。

2. 遍历文本 text

 for i in range(len(text)):

遍历文本 text 中的每个字符,逐个比较是否匹配模式串。

3. 回退逻辑 

while count > 0 and pattern[count] != text[i]: count = maxMatchLengths[count - 1] 
  • 如果当前字符不匹配,则利用 π 数组回退到上一个可能的匹配位置。
  • 如果 count == 0,说明当前没有部分匹配,直接从头开始重新匹配。

4. 匹配逻辑

if pattern[count] == text[i]: count += 1

  • 如果当前字符匹配,扩展匹配长度 count

5. 完整匹配 

if count == len(pattern):
    positions.append(i - len(pattern) + 1)
    count = maxMatchLengths[count - 1]

  • count == len(pattern) 时,说明模式串完整匹配。
  • 记录匹配的起始位置为 i - len(pattern) + 1
  • 利用 π 数组调整 count,以寻找下一个可能的匹配。

完整代码:

class Solution:

    def creatneedle(self,mystr:str)->list:

        mylist=[0]*len(mystr)

        maxlength=0

        for i in range(1,len(mystr)):

            while maxlength>0 and mystr[i]!=mystr[maxlength]:

                maxlength=mylist[maxlength-1]

            if mystr[i]==mystr[maxlength]:

                maxlength+=1

            mylist[i]=maxlength

        return mylist

    def strStr(self, haystack: str, needle: str) -> int:

        mylist = self.creatneedle(needle)

        count = 0

        for i in range(len(haystack)):

            while count>0 and needle[count]!=haystack[i]:

                count=mylist[count-1]

            if needle[count]==haystack[i]:

                count+=1

            if count==len(needle):

                return i-len(needle)+1

        return -1

附灵神企业级理解:

作者:灵茶山艾府
链接:https://www.zhihu.com/question/21923021/answer/37475572
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

角色:

甲:abbaabbaaba
乙:abbaaba

乙对甲说:「帮忙找一下我在你的哪个位置。」

甲从头开始与乙一一比较,发现第 7 个字符不匹配。

要是在往常,甲会回退到自己的第 2 个字符,乙则回退到自己的开头,然后两人开始重新比较。[1]这样的事情在字符串王国中每天都在上演:不匹配,回退,不匹配,回退,……

但总有一些妖艳字符串要花自己不少的时间。

上了年纪的甲想做出一些改变。于是乎定了个小目标:发生不匹配,自身不回退。

甲发现,若要成功与乙匹配,必须要匹配 7 个长度的字符。所以就算自己回退到第 2 个字符,在后续的匹配流程中,肯定还会重新匹配到自己的第 7 个字符上。

当在甲的某个字符 c 上发生不匹配时,甲即使回退,最终还是会重新匹配到字符 c 上。

那干脆不回退,岂不美哉!

甲不回退,乙必须回退地尽可能少,并且乙回退位置的前面那段已经和甲匹配,这样甲才能不用回退。

如何找到乙回退的位置?

「不匹配发生时,前面匹配的那一小段 abbaab 于我俩是相同的」,甲想,「这样的话,用 abbaab 的头部去匹配 abbaab 的尾部,最长的那段就是答案。」

abbaab 的头部有 a, ab, abb, abba, abbaa(不包含最后一个字符。下文称之为「真前缀」)
abbaab 的尾部有 b, ab, aab, baab, bbaab(不包含第一个字符。下文称之为「真后缀」)
这样最长匹配是 ab。也就是说甲不回退时,乙需要回退到第三个字符去和甲继续匹配。

「要计算的内容只和乙有关」,甲想,「那就假设乙在其所有位置上都发生了不匹配,乙在和我匹配前把其所有位置的最长匹配都算出来(算个长度就行),生成一张表,之后我俩发生不匹配时直接查这张表就行。」

据此,甲总结出了一条规则并告诉了乙:

所有要与甲匹配的字符串(称之为模式串),必须先自身匹配:对每个子字符串 [0...i],算出其「相匹配的真前缀与真后缀中,最长的字符串的长度」。

「小 case,我对自己还不了解吗」,乙眨了一下眼睛,「那我回退到第三个字符和你继续匹配吧~」


新的规则很快传遍了字符串王国。现在来看看如何高效地计算这条规则。这里有个很好的例子:abababzabababa。

列个表手算一下:(最大匹配数为子字符串 [0...i] 的最长匹配的长度)

子字符串  a b a b a b z a b a b a b a
最大匹配数 0 0 1 2 3 4 0 1 2 3 4 5 6 ?

一直算到 6 都很容易。在往下算之前,先回顾下我们所做的工作:

对子字符串 abababzababab 来说,

真前缀有 a, ab, aba, abab, ababa, ababab, abababz, ...

真后缀有 b, ab, bab, abab, babab, ababab, zababab, ...

所以子字符串 abababzababab 的真前缀和真后缀最大匹配了 6 个(ababab),那次大匹配了多少呢?

容易看出次大匹配了 4 个(abab),更仔细地观察可以发现,次大匹配必定在最大匹配 ababab 中,所以次大匹配数就是 ababab 的最大匹配数!

直接去查算出的表,可以得出该值为 4。

第三大的匹配数同理,它既然比 4 要小,那真前缀和真后缀也只能在 abab 中找,即 abab 的最大匹配数,查表可得该值为 2。

再往下就没有更短的匹配了。

回顾完毕,来计算 ? 的值:既然末尾字母不是 z,那么就不能直接 6+1=7 了,我们回退到次大匹配 abab,刚好 abab 之后的 a 与末尾的 a 匹配,所以 ? 处的最大匹配数为 5。


上 Java 代码,它已经呼之欲出了:

// 构造模式串 pattern 的最大匹配数表
int[] calculateMaxMatchLengths(String pattern) {int[] maxMatchLengths = new int[pattern.length()];int maxLength = 0;for (int i = 1; i < pattern.length(); i++) {while (maxLength > 0 && pattern.charAt(maxLength) != pattern.charAt(i)) {maxLength = maxMatchLengths[maxLength - 1]; // ①}if (pattern.charAt(maxLength) == pattern.charAt(i)) {maxLength++; // ②}maxMatchLengths[i] = maxLength;}return maxMatchLengths;
}

有了代码后,容易证明它的复杂度是线性的(即运算时间与模式串 pattern 的长度是线性关系):由 ② 可以看出 maxLength 在整个 for 循环中最多增加 pattern.length() - 1 次,所以让 maxLength 减少的 ① 在整个 for 循环中最多会执行 pattern.length() - 1 次,从而 calculateMaxMatchLengths 的复杂度是线性的。

KMP 匹配的过程和求最大匹配数的过程类似,从 count 值的增减容易看出它也是线性复杂度的:

// 在文本 text 中寻找模式串 pattern,返回所有匹配的位置开头
List<Integer> search(String text, String pattern) {List<Integer> positions = new ArrayList<>();int[] maxMatchLengths = calculateMaxMatchLengths(pattern);int count = 0;for (int i = 0; i < text.length(); i++) {while (count > 0 && pattern.charAt(count) != text.charAt(i)) {count = maxMatchLengths[count - 1];}if (pattern.charAt(count) == text.charAt(i)) {count++;}if (count == pattern.length()) {positions.add(i - pattern.length() + 1);count = maxMatchLengths[count - 1];}}return positions;
}

最后总结下这个算法:

  1. 匹配失败时,总是能够让模式串回退到某个位置,使文本不用回退。
  2. 在字符串比较时,模式串提供的信息越多,计算复杂度越低。(有兴趣的可以了解一下 Trie 树,这是文本提供的信息越多,计算复杂度越低的一个例子。)

参考链接:(36 封私信 / 80 条消息) 如何更好地理解和掌握 KMP 算法? - 知乎 

灵神题单:分享|如何科学刷题? - 力扣(LeetCode) 

 

 

 

 

 

 

 

 

 

 

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

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

相关文章

MySQL5.7.37安装配置

1.下载MySQL软件包并解压 2.配置环境变量 3.新建my.ini文件并输入信息 [mysqld] #端口号 port 3306 #mysql-5.7.27-winx64的路径 basedirC:\mysql-5.7.37\mysql-5.7.37-winx64 #mysql-5.7.27-winx64的路径\data datadirC:\mysql-5.7.37\mysql-5.7.37-winx64\data #最大连接数…

基于单片机的手持金属探测仪设计

本设计以STM32F103C8T6单片机为核心&#xff0c;通过金属线圈感应器来判断是否存在金属&#xff0c;控制OLED显示屏显示金属探测仪的灵敏度和参考值&#xff0c;通过电源模块将220V转化为3.3V对单片机进行供电&#xff0c;还可以通过按键对金属探测仪的灵敏度进行设置&#xff…

P1197 星球大战(并查集+逆向思维)

这是今天写的比较有价值的一道题&#xff0c;晚上写了大概一个多小时&#xff0c;主要还是在debug&#xff0c;出得很妙&#xff0c;好题&#x1f44d; P1197 [JSOI2008] 星球大战 - 洛谷 | 计算机科学教育新生态 思路&#xff1a;如果我们按照顺序一个一个的去计算毁灭一个星…

深度学习驱动的蛋白质设计技术与前沿实践-从基础到尖端应用

RoseTTAFold&#xff0c;作为David Baker教授团队早期开发的蛋白质结构预测工具&#xff0c;在学术界与工业界广受认可。然而&#xff0c;随着时间推移&#xff0c;仅局限于预测已知结构的蛋白质并不能满足生物医药和生物工程领域对创新设计的需求。这促使David Baker教授团队继…

Linux 进程信号初识

目录 0.前言 1.什么是信号 1.1生活中的信号 1.2 OS中的信号 2.认识信号 2.1信号概念 2.2查看信号 2.3 signal函数 2.4代码示例 3. 信号处理方式 3.1 忽略信号 3.2 默认处理 3.3 自定义处理 4.小结 &#xff08;图像由AI生成&#xff09; 0.前言 在之前的学习中&#xff0c;我…

SpringBoot(二十五)SpringBoot集成JRebel实现热更新

今天来安装一个IDEA代码热更新的插件,一个神器。 我们之前也为IDEA配置了热更新,使用的是spring-boot-devtools插件。具体请移步《SpringBoot(一)创建项目及配置IDEA热更新》 上边这个热更新对于单模块项目是没有问题的,但是对于多模块项目可能就无能无能为力了,而且,随…

MATLAB中的绘图技巧

MATLAB作为一种强大的科学计算软件&#xff0c;不仅可以进行数据分析和模拟&#xff0c;还具有出色的绘图功能。本文介绍若干在MATLAB中绘图的技巧和方法&#xff0c;帮助使用者更好地呈现数据和结果 文章目录 基本绘图函数高级绘图技巧三维绘图动态绘图绘图工具结语 基本绘图函…

java八股-AQS,Reentrantlock

什么是AQS&#xff1f; 难度&#xff1a;★★★☆☆ 考频&#xff1a;★★★☆☆ 注意这个队列是双向队列&#xff0c;每次有线程释放锁了之后&#xff0c;会有下一个线程来&#xff0c;以及队列头元素&#xff0c;如果设置的是公平锁&#xff0c;那么是等了很久的头元素先获…

python——模块 迭代器 正则

一、python模块 先创建一个 .py 文件&#xff0c;这个文件就称之为 一个模块 Module。 使用模块的优点&#xff1a; 模块化编程&#xff0c;多文件编程 1.2 模块的使用 1.2.1 import语句 想要B.py文件中&#xff0c;使用A.py文件&#xff0c;只需要在B.py文件中使用关键字…

STL之mapset|AVL树

STL之map&set|AVL树 set&map搜索二叉树实现代码 set的使用map的使用set&map的模拟实现&#xff08;见红黑树篇&#xff09; AVL树AVL树的模拟实现 set&map 前言&#xff1a;stl库中set和map的底层都是红黑树&#xff0c;一种平衡搜索二叉树&#xff0c;是我下…

使用阿里云快速搭建 DataLight 平台

使用阿里云快速搭建 DataLight 平台 本篇文章由用户 “闫哥大数据” 分享&#xff0c;B 站账号&#xff1a;https://space.bilibili.com/357944741?spm_id_from333.999.0.0 注意&#xff1a;因每个人操作顺序可能略有区别&#xff0c;整个部署流程如果出现出入&#xff0c;以…

OceanBase 分区表详解

1、分区表的定义 在OceanBase数据库中&#xff0c;普通的表数据可以根据预设的规则被分割并存储到不同的数据区块中&#xff0c;同一区块的数据是在一个物理存储上。这样被分区块的表被称为分区表&#xff0c;而其中的每一个独立的数据区块则被称为一个分区。 如下图所示&…

代码随想录算法训练营第三十八天 | 322.零钱兑换 279.完全平方数 139.单词拆分 多重背包以及背包总结

LeetCode 322.零钱兑换&#xff1a; 文章链接 题目链接&#xff1a;322.零钱兑换 思路&#xff1a; 首先分析题目&#xff0c;每种硬币的数量是无限的&#xff0c;因此为完全背包问题&#xff1b;又要求返回的是最少硬币个数&#xff0c;因此与组合数/排列数无关&#xff0c…

计算机网络WebSocket——针对实习面试

目录 计算机网络WebSocket什么是WebSocket&#xff1f;WebScoket和HTTP协议的区别是什么?说明WebSocket的优势和使用场景&#xff1f;说明WebSocket的建立连接的过程&#xff1f; 计算机网络WebSocket 什么是WebSocket&#xff1f; WebSocket是一个网络通信协议&#xff0c;提…

在Ubuntu 24.04 LTS上安装飞桨PaddleX

前面我们介绍了《在Windows用远程桌面访问Ubuntu 24.04.1 LTS》本文接着介绍安装飞桨PaddleX。 PaddleX 3.0 是基于飞桨框架构建的一站式全流程开发工具&#xff0c;它集成了众多开箱即用的预训练模型&#xff0c;可以实现模型从训练到推理的全流程开发&#xff0c;支持国内外多…

LM2 : A Simple Society of Language Models Solves Complex Reasoning

文章目录 题目摘要简介相关工作方法论实验结果结论局限性 题目 LM2&#xff1a;简单的语言模型社会解决复杂推理问题 论文地址&#xff1a;https://aclanthology.org/2024.emnlp-main.920/ 项目地址&#xff1a; https://github.com/LCS2-IIITD/Language_Model_Multiplex 摘要…

(三十三)队列(queue)

文章目录 1. 队列&#xff08;queue&#xff09;1.1 定义1.2 函数1.3 习题1.3.1 例题&#xff08;周末舞会&#xff09; 2. 双向队列&#xff08;deque&#xff09;2.1 定义2.2 函数2.3 题目2.3.1 例题&#xff08;打BOSS&#xff09; 1. 队列&#xff08;queue&#xff09; 队…

web——upload-labs——第二关

MIME验证 MIME&#xff08;Multipurpose Internet Mail Extensions&#xff09;验证是指在互联网传输中&#xff0c;通过检查数据的MIME类型来确保数据格式的正确性和安全性。MIME最初是为了扩展电子邮件的功能&#xff0c;让邮件支持多种格式&#xff0c;如文本、图片、音频等…

Vue3 -- 集成sass【项目集成5】

集成sass&#xff1a; 看过博主的 配置styleLint工具应该已经安装过 sass sass-loader 了&#xff0c;所以我们只需要加上我们的 lang"scss"即可。 <style scoped lang"scss"></style>给项目添加全局样式文件&#xff1a; 在src文件夹下创建…

【Web前端】Promise的使用

Promise是异步编程的核心概念之一。代表一个可能尚未完成的操作&#xff0c;并提供了一种机制来处理该操作最终的成功或失败。具体来说&#xff0c;Promise是由异步函数返回的对象&#xff0c;能够指示该操作当前所处的状态。 当Promise被创建时&#xff0c;它会处于“待定”&a…