KMP算法PMT数组与next数组构造解释

2023-11-27 10:10

本文主要是介绍KMP算法PMT数组与next数组构造解释,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

从零开始,静心学习

 
 

1. 前言

KMP算法是用于搜索子串的经典算法,其中重点就在于利用了next数组减少了很多重复的搜索,这里不细讲KMP算法是怎么进行搜索的,我尽可能地将next的数组构造中的一些当时令我困惑的问题讲解清楚。

 
 

2. PMT(Partial Match Table)数组定义

首先,我觉得这next数组构造一部分很模糊的主要原因是很多定义大家都不讲清楚,这里我只讲自己的理解方法。这里我先不讲next数组。

首先,KMP算法要用到最长公共前后缀的长度。

解释一
最长公共前后缀:首先,这里我们都把前缀和后缀都认为是真前缀和真后缀。
举个例子:有一个字符串"aabaaf",它的前缀有{“a”, “aa”, “aab”, “aaba”, “aaaa”},我们不认为字符串本身"aabaaf"是它的前缀。同理它的后缀有{“abaaf”, “baaf”, “aaf”, “af”, “f”},我们不认为字符串本身"aabaaf"是它的后缀。可以看到字符串"aabaaf"并没有公共的前后缀,所以最长的公共前后缀的长度为0。

不仅如此,对于字符串"aabaaf",我们还需要知道它的所有从头开始的子串(这里包括{“a”, “aa”, “aab”, “aaba”, “aabaa”, “aabaaf”})的最长公共前后缀的长度,这个时候我们就需要用到PMT数组。

解释二
下面的表就是该字符串的PMT数组。这里我强调非常非常重要的一点,即PMT[i]表示的是直到索引为i的子串p[: i + 1]的最长公共前后缀的长度。举个例子,比如PMT[4] = 2表示的是"aabaa"的最长公共前后缀的长度,而不是"aaba"的最长公共前后缀的长度。“aabaa"的前缀有{“a”, “aa”, “aab”, “aaba”},后缀有{“abaa”, “baa”, “aa”, “a”},公共前后缀有{“a”, “aa”},最长的公共前后缀为"aa”,所以PMT[4]的值就是"aa"的长度,即PMT[4] = 2

charaabaaf
index012345
value010120

 
 

3. PMT数组的求解

接下来就是怎么求解一个字符串的PMT数组。

步骤一

首先对所有的非空字符串都有PMT[0] = 0,所以我们从下标i = 1开始处理。
1

解释三
这里的主串和子串都是模式串,其中找最长公共前后缀的方式与文本串与模式串的匹配类似。
 
为了找到当前位置的最长公共前后缀,我们可以认为主串是在找相同的后缀,子串只在找相同的前缀。在上面的图中,B就是前缀,C就是后缀。又因为主串和子串完全相同,所以A == B,又由于我们在匹配过程中一直B == C,所以一定有A == B。所以看似是两个字符串之间的比对,但实际上还是找同一个字符串,也就是模式串的最长公共前后缀。

上图中i, j = 1, 0时有p[i] == p[j],此时我们很自然地能够得到一条规律,PMT[i] = PMT[i - 1] + 1并且有i += 1, j += 1

步骤二

当i与j都前进一步后,我们就会遇到下面的情况。
2

此时i, j = 2, 1,我们匹配p[i]p[j]。如果相等,那么和前面的情况一样,当前位置的PMT值PMT[i] = PMT[i - 1] + 1,加一就行了。
但是当前的p[i] != p[j],这时候我们需要其他的操作。这里我用其他位置处的匹配情况来说。

步骤三

3

此时后缀B == 前缀C,和第一幅图一样,这时都将i和j前进一步,但是前进一步,就会遇到第二幅图所出现的情况。
4
此时后缀B != 前缀C,所以我们这是要将眼光放在该索引位置前面的前缀与后缀。我们再看下面一个图。
5
p[5] != p[2]时我们该怎么办呢?我们要想到我们是要找当前位置的最长公共前后缀,所以我们要关注当前位置前面的PMT值,也就是PMT[j - 1]。我们尝试在旧后缀与旧前缀里面找到他们的最长公共前后缀。因为B == C,又因为E是B的最长公共后缀,F是C的最长公共前缀,所以我们一定有E == F。所以我们实际上也就是要比较新的后缀E后面的一个字符"f"与新的前缀F后面的一个字符"a"是否一样。我们知道"f"就是当前p[i]的取值,而怎么得到前缀F后面一个字符呢,实际上PMT[j - 1]就是这里的F后面的"a"的索引。所以这就是索引j的跳转规律,即j = PMT[j - 1]

所以我们会得到下面的状态。
6

这是我们比较主串的p[5]与子串的p[1]是否相同,然而很不幸,还是不相同。这个时候同样的,我们的索引j还要跳转,此时j = PMT[j - 1] = PMT[0] = 0,就会得到下图。
7
这个时候p[5] != p[0],这个时候我们按照之前的规律j还是要跳转的,但是此时如果跳转j = PMT[j - 1],而此时PMT[j - 1] = PMT[-1],也即是j就要跳转到PMT[-1]的值。在python里这个指的是最后索引位置处的值,然而这是不合理的。所以我们还要加一条规则,j = 0时,如果p[i] == p[j],那么可以继续往后匹配;如果p[i] != p[j],那么这个时候就表明当前索引位置上的PMT值PMT[i] = 0

还有最后一个问题,在上面的规则中,如果在代码中写PMT[i] = PMT[i - 1] + 1,这样得到的实际上并不是正确的结果。因为这样的前提是你的索引j没有跳转过,才会有这个规则。那么怎么改变呢?在j没有跳转过的情况下有j = PMT[i - 1],而j跳转过的情况下有PMT[i] = j + 1,所以最终的代码如下。

# 原始版
def get_pmt(p):n = len(p)my_pmt = [0] * ni, j = 1, 0while i < n:if p[i] == p[j]:my_pmt[i] = j + 1i += 1j += 1else:if j == 0:i += 1else:j = my_pmt[j - 1]return my_pmt# 简化版
def get_pmt(p):n = len(p)my_pmt = [0] * ni, j = 1, 0while i < n:while j > 0 and p[i] != p[j]:j = my_pmt[j - 1]if p[i] == p[j]:j += 1my_pmt[i] = ji += 1return my_pmt

 
 

4. next数组的定义

首先声明,实际上我觉得next数组没什么用。在实际用kmp算法时,能够得到上面的PMT我觉得就足够了,没必要再去纠结减一还是不减一,右移还是不右移。

第二,关于next数组的定义,本文采用如下形式:
n e x t = { − 1 , j = 0 m a x { k ∣ 1 < k < j , p [ 0 ] . . . p [ k − 1 ] = = p [ j − k ] . . . p [ j − 1 ] } 0 , o t h e r next = \left \{ \begin{matrix} -1, j = 0 \\ max\{k|1<k<j, p[0]...p[k - 1] == p[j - k]...p[j - 1]\} \\ 0, other \end{matrix} \right. next=1,j=0max{k1<k<j,p[0]...p[k1]==p[jk]...p[j1]}0,other

这里的next数组相比较上面的PMT数组一定是右移了一位的,即PMT[i] = next[i + 1],然后设置next[0] = -1,但是后续的next数组里面的值是否还要减一这个是不一定的,这个在写代码的时候可以灵活调整。有的文章说设置next[0] = 0会陷入死循环,但实际上在写代码时只要加上个if j == 0的判断同样是可以避免的,但是我们按照定义来还是将next[0]设为-1。最终我的结论是PMT数组没必要右移一位形成next数组,next数组也没必要在自己身上再减去一(注:这里不认同PMT数组与next数组是同一个对象,同时也不认同PMT数组在不右移的情况下自身减一然后形成next数组)。

 
 

4. next数组的求解

这里贴两个自己写的求next数组的代码,分别是PMT数组右移后不减一形成的next数组PMT数组右移后再减一形成的next数组。

# 开头设置next[0] = -1,但是后续数组并不减一(即右移不减一)
def get_next(p):n = len(p)my_next = [0] * nmy_next[0] = -1i, j = 1, 0while i < n - 1:while j > 0 and p[i] != p[j]:j = my_next[j]if p[i] == p[j]:j += 1my_next[i + 1] = ji += 1return my_next# 开头设置next[0] = -1,同时后续数组减一(即右移再减一)
def get_next(p):n = len(p)my_next = [0] * nmy_next[0] = -1i, j = 1, 0while i < n - 1:while j > 0 and p[i] != p[j]:j = my_next[j]if p[i] == p[j]:j += 1my_next[i + 1] = j - 1  # 实际上就是这里把j换成j - 1i += 1return my_next

最后再贴两段感想

注意我们确实求出了字符串的每个位置上的pmt的值。但是在实际利用pmt数组进行搜索时,我们是用不到pmt数组的最后一位的,即用不到pmt[n - 1]。因为假设模式串p的长度为n,并且在最后一位之前都匹配成功。这个时候我们指针i, j都右移一位,此时j = n - 1,即要匹配模式串都最后一个字符。此时很显然只有两种情况。如果匹配成功,则直接返回找到了该模式串,并且返回索引值;而如果没匹配成功,我们要调用的是pmt[j - 1] = pmt[n - 2]。所以我们是无论如何也使用不到pmt数组的最后一位的。但是注意,我们是有可能用到pmt[0]的。

首先我们要明确next数组一定是pmt数组右移来的。假设字符串p的长度为n,则next[i] = pmt[i - 1], i = 1, …, n - 2。所以pmt数组能用到pmt[0],但用不到pmt[n - 1];而next数组能用到next[n - 1],却用不到next[0]。但是next[0]是否一定要取0是不一定的。注意,这里即使tmp数组右移成了next数组后,next[0]也是可以取0的。

这篇关于KMP算法PMT数组与next数组构造解释的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/427482

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu2241(二分+合并数组)

题意:判断是否存在a+b+c = x,a,b,c分别属于集合A,B,C 如果用暴力会超时,所以这里用到了数组合并,将b,c数组合并成d,d数组存的是b,c数组元素的和,然后对d数组进行二分就可以了 代码如下(附注释): #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<que

wolfSSL参数设置或配置项解释

1. wolfCrypt Only 解释:wolfCrypt是一个开源的、轻量级的、可移植的加密库,支持多种加密算法和协议。选择“wolfCrypt Only”意味着系统或应用将仅使用wolfCrypt库进行加密操作,而不依赖其他加密库。 2. DTLS Support 解释:DTLS(Datagram Transport Layer Security)是一种基于UDP的安全协议,提供类似于

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

poj 3974 and hdu 3068 最长回文串的O(n)解法(Manacher算法)

求一段字符串中的最长回文串。 因为数据量比较大,用原来的O(n^2)会爆。 小白上的O(n^2)解法代码:TLE啦~ #include<stdio.h>#include<string.h>const int Maxn = 1000000;char s[Maxn];int main(){char e[] = {"END"};while(scanf("%s", s) != EO

hdu 1166 敌兵布阵(树状数组 or 线段树)

题意是求一个线段的和,在线段上可以进行加减的修改。 树状数组的模板题。 代码: #include <stdio.h>#include <string.h>const int maxn = 50000 + 1;int c[maxn];int n;int lowbit(int x){return x & -x;}void add(int x, int num){while

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费