超简单理解KMP算法(最长公共前后缀next数组、合并主子串、子串偏移法)
创作时间:
作者:
@小白创作中心
超简单理解KMP算法(最长公共前后缀next数组、合并主子串、子串偏移法)
引用
CSDN
1.
https://m.blog.csdn.net/m0_53632564/article/details/145783773
KMP算法是字符串匹配领域的重要算法,其核心思想是通过预处理模式串(子串)来避免不必要的字符比较,从而提高匹配效率。本文将从最长公共前后缀next数组、合并主子串、子串偏移法三个方面深入解析KMP算法的原理和实现。
最长公共前后缀next
这个概念是一个巧妙的技巧,帮助我们在遍历数组时记录其相似特性。虽然这个想法非常巧妙,但其背后的逻辑确实不容易理解。
前缀和后缀定义
- 前缀:不包含最后一个字符的所有以第一个字符开头的连续子串。
- 后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串。
例如,对于字符串 "ababc":
- 前缀有: "a"、"ab"、"aba"、"abab"
- 后缀有: "c"、"bc"、"abc"、"babc"
最长公共前后缀
最长公共前后缀是指某个字符串的前缀和后缀中相等的最长子串。例如:
- 对于 "abab",最长公共前后缀是 "ab",长度为 2。
- 对于 "abc",没有公共前后缀,长度为 0。
next数组计算
next数组用于记录每个位置的最长公共前后缀的长度。例如,对于模式串 "ababc",其前缀表为 [0, 0, 1, 2, 0]:
- 第 0 位: "a",没有前后缀,值为 0。
- 第 1 位: "ab",没有公共前后缀,值为 0。
- 第 2 位: "aba",最长公共前后缀为 "a",值为 1。
- 第 3 位: "abab",最长公共前后缀为 "ab",值为 2。
- 第 4 位: "ababc",没有公共前后缀,值为 0。
计算next数组的递推性质:
假设我们希望求next[i],并知道next[i-1]=len,那说明[0,i-1]的子串的首尾均有长度为len的前后缀:
- 如果str[i]==str[len],说明[0,i]的子串的首尾均有len+1长度的前后缀,得到next[i] =len+1(注意下标从0开始所以比较的是str[len])
- 如果str[i]!=str[len],我们继续寻找[0,i-1]的子串中第二长、第三长的前缀串粉色段,这些字符串虽然长度小于len,但起点和终点依然是0和j-1,因为next[i]找到的前后缀一定要经过0和j
- 把寻找的过程可以写成while,目标是找到尽量长的粉色段n,并满足str[n]=str[i]
- 现在问题是n怎么获得?我们最初知道next[i-1]=len,所以绿色=橙色,而现在又找到了蓝色=橙色,所以有蓝色=绿色,所以粉色段恰恰可以用next[len-1]表示,当找到了粉色段但不满足str[n]=str[i]时,继续找下一个粉色段,即next[n-1],如此循环
- 最终可以得到next数组,方法如下:
def getNext(self, next, s):
for i in range(1, len(s)):
len_ = next[i - 1]
while len_ != 0 and s[i] != s[len_]: #等于0也是没有,会跳过的
len_ = next[len_ - 1]
if s[i] == s[len_]: #跳出循环要不是0要不相等
next[i] = len_ + 1
vector<int> pi(str.size());
for (int i = 1; i < str.size(); i++) {
int len = pi[i-1];
while (len != 0 && str[i] != str[len]) {
len = pi[len - 1];
}
if (str[i] == str[len]) {
pi[i] = len + 1;
}
}
合并主子串
这个问题要求在haystack字符串中找出needle字符串的第一个匹配项的下标。如果needle不是haystack的一部分,则返回-1。
将haystack和needle合并,中间用#分隔,求合并串的next数组,如果数组中有值等于len(needle),则说明有匹配项。这种方法可以直接在next数组获得的过程中判断,代码如下:
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
n = len(haystack) #主串的的长度
m = len(needle) #子串的长度
if haystack == needle: #因为i从1开始,所以处理edge case
return 0
s = needle + "#" + haystack #注意把子串放前面,这样前缀和才能覆盖子串
next = [0] * len(s)
for i in range(1, n + m + 1): #注意+1是因为"#"多了一位
len_ = next[i - 1]
while len_ != 0 and s[i] != s[len_]: #等于0也是没有,会跳过的
len_ = next[len_ - 1]
if s[i] == s[len_]: #跳出循环要不是0要不相等
next[i] = len_ + 1
if next[i] == m:#返回第一个
return i - m*2 #合并串从第m+1位开始才是主串,所以主串中开始匹配的下标是i - (m+1) - m + 1=i-2*m
return -1
子串偏移
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。子串偏移方法一句话就是利用子串的前后缀和,保证下一个可能匹配的地方就是主串的后缀。
- 比如第i位AC虽然没有匹配上,但我们知道[0,i-1]的内容是一样的,如何复用这一信息?注意i指的是子串指针,这里图示主串和子串指针是一样的所以没有区分,但本质要利用子串next数组的前后缀相同,所以要用子串指针
- 为了和暴力方法区分,我们不希望直接把主串的指针移到下一个位置开始遍历,而是一直向前方移动,这需要我们移动指针到仍然有可能重新匹配的地方,如何寻找?利用next数组和[0,i-1]的内容的已知信息
- [0,i-1]中的前后缀长度是next[i-1],而主串匹配子串最基本的要求就是从第一个字符相等。现在主串的头需要移动(因为0作为起点不行了),我们又不像只移动一个,这时可以想到主串的后缀恰好等于子串的开头,举个例子
- 主串是ABCABDC,子串是ABCABC,i此时是5,我们已知[0,4]的ABCAB相等和next[4]=2。主串把0当起点时找不到匹配的,但此时主串[0,4]的末尾和子串的开头相等,都是AB,所以把主串的起点移动到i-next[i-1]=5-2=3后,主串和子串又有了next[i-1]长度的相等段(因为next数组的定义,0-3之间不可能有另外等于子串开头的部分,前后缀一定会覆盖到第一个可能的起点处,如ABACAB匹配ABACAD时,不可能把起点移到中间的A,因为下一位是C,前后缀在计算时就比较过了)
- 这样等价于主串不动,还是从第i位开始比较,而子串从则回退到next[i-1]位,因为子串的[0,i-1)已知和主串是相等的,回到图中的例子就是:
这种方法的代码如下:
class Solution:
def getNext(self, next, s):
for i in range(1, len(s)):
len_ = next[i - 1]
while len_ != 0 and s[i] != s[len_]: #等于0也是没有,会跳过的
len_ = next[len_ - 1]
if s[i] == s[len_]: #跳出循环要不是0要不相等
next[i] = len_ + 1
def strStr(self, haystack: str, needle: str) -> int:
next = [0] * len(needle)
self.getNext(next, needle)
i = 0 #主串指针
j = 0 #子串指针
for i in range(len(haystack)):
while j > 0 and haystack[i] != needle[j]:#不匹配,子串到下一个可能匹配的地方next[j-1],注意只要j>0要一直找,而不是只试一次
j = next[j - 1]
if haystack[i] == needle[j]:#字符匹配,指针后移
j += 1
if j == len(needle):#在主串中找到了子串
return i - len(needle) + 1
return -1
热门推荐
牛奶变酸了,还能喝吗?小心有害细菌!
变酸牛奶别扔!自制酸奶&奶酪攻略
CRISPR治疗艾滋病和白血病,中国实现全球首次临床应用
墙体彩绘点亮城市文化:从老旧街区到智能公共艺术
<万里归途>:真实还原外交官撤侨,获外交部点赞
基因甲基化检测:消化道癌症早筛的新突破
樊嘉院士揭秘:功能性精准医学如何改变癌症治疗
Nature Biotech推荐:NeoDisc精准治疗新突破
肿瘤生物标志物助力癌症精准治疗:从华南理工突破看未来医疗新趋势
《英雄联盟》全屏快捷键攻略:提升游戏体验的关键技巧
微信账号异常登录怎么办?这份实用指南请收好
微信加密新姿势:设置服务密码锁和应用锁,守护隐私与财产安全
掌握快捷键,轻松搞定全屏模式
真人奇幻VS动画喜剧:两部新片各展风采
买了PS5之后不知道玩啥的小伙伴看过来!盘点PS5上神级游戏佳作
十道排骨食谱,一周不重样,健康美味全家人点赞!
冬季养生必备:排骨的N种吃法
炖排骨还是红烧排骨更好吃?两种经典排骨做法大比拼
泉港“头北话”:泉州话与莆仙话融合而成的独特方言
黑色喜剧《无名之辈2》开机,刘德华加盟成最大看点
<万里归途>:15.63亿票房背后的真实还原与演员演技
银行理财和标品信托:提升个人收入的两大利器
侧步躲闪:最实用的防身术之一,一文掌握要点
珍珠泉度假区:南京浦口必打卡景点
打卡浦口最美景点:老山&珍珠泉
浦口区三大古寺:泰山寺、兜率寺、惠济寺
揭秘沃克中将在朝鲜战争中的致命失误
中国天眼与韦布望远镜:观测宇宙的双璧
伽利略式望远镜:从历史到现代的观星之旅
智能化运力优化推动物流降本增效,社会物流成本占比降至14.4%