一文搞懂KMP算法!!!

🍁什么是KMP算法?

  • KMP算法是一种改进的 字符串匹配算法,由 D.E.KnuthJ.H.MorrisV.R.Pratt 提出的,因此人们称它为 克努特—莫里斯—普拉特 操作(简称 KMP 算法)。
    • KMP 算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
    • 具体实现就是通过一个 next() 数组实现,数组本身包含了模式串的局部匹配信息。KMP 算法的时间复杂度 O ( m + n ) O(m+n) O(m+n)

🍁什么是 next() 数组 和 前缀表?

next 数组就是一个前缀表(prefix table)!

前缀表有什么作用呢

前缀表是用来回退的,它记录了 模式串主串(文本串) 不匹配的时候,模式串 应该从哪里开始重新匹配。

我们来举一个例子:

  • 要在文本串aabaabaafa 中查找是否出现过一个模式串aabaaf

如动画所示

在这里插入图片描述

最长公共前后缀

  • 字符串的前缀是:指不包含最后一个字符的所有 以第一个字符开头的连续子串
  • 后缀:是指不包含第一个字符的所有 以最后一个字符结尾的连续子串

正确理解什么是前缀什么是后缀很重要!
可以理解为最长相等前后缀

如何计算前缀表

注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。

  1. 长度为前1个字符的子串 a,最长相同前后缀的长度为 0

在这里插入图片描述

  1. 长度为前2个字符的子串 aa ,最长相同前后缀的长度为 1

在这里插入图片描述
3. 长度为前3个字符的子串 aab,最长相同前后缀的长度为 0

在这里插入图片描述

  1. 以此类推: 长度为前4个字符的子串 aaba,最长相同前后缀的长度为 1
  2. 长度为前5个字符的子串aabaa,最长相同前后缀的长度为 2
  3. 长度为前6个字符的子串aabaaf,最长相同前后缀的长度为 0

在这里插入图片描述

🚀 构造next数组

构造 next 数组其实就是计算 模式串 s

  • 定义两个指针 ijj 指向前缀末尾位置i 指向后缀末尾位置
  • next[i] 表示 i(包括 i )之前最长相等的前后缀长度(其实就是 j)。

next 数组就可以是前缀表,但是很多实现都是把 前缀表统一减一(右移一位,初始位置为 -1 )之后作为 next 数组。前缀表的构造过程,主要有如下三步:

  1. 初始化:
    • j 初始化为 -1;
  2. 处理前后缀不相同的情况
    • 因为 j 初始化为 -1,那么 i 就从1 开始,进行 s[i]s[j+1] 的比较;
    • 如果 s[i]s[j+1] 不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。
      • next[j] 就是记录着 j(包括 j )之前的子串的相同前后缀的长度;
      • 那么 s[i]s[j+1] 不相同,就要找 j+1 前一个元素在 next 数组里的值(就是 next[j] )。
  3. 处理前后缀相同的情况
    • 如果 s[i]s[j + 1] 相同,那么就同时向后移动 ij 说明找到了相同的前后缀,同时还要将 j(前缀的长度)赋给 next[i] , 因为 next[i] 要记录相同前后缀的长度。

构造 next 数组的逻辑流程动画如下:
在这里插入图片描述
构造 next 数组的函数如下:(C++)

void getNext(int* next, const string& s){
    int j = -1;
    next[0] = j;
    for(int i = 1; i < s.size(); i++) { // 注意i从1开始
        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
            j = next[j]; // 向前回退
        }
        if (s[i] == s[j + 1]) { // 找到相同的前后缀
            j++;
        }
        next[i] = j; // 将j(前缀的长度)赋给next[i]
    }
}

🚀 使用next数组来做匹配

在文本串 s 里找是否出现过模式串 t

  • 定义两个下标 j 指向模式串起始位置,i 指向文本串起始位置;
  • j 初始值依然为 -1
  • 接下来就是 s[i]t[j + 1] (因为 j-1 开始的)进行比较:
    • 如果 s[i]t[j + 1] 不相同,j 就要从 next 数组里寻找下一个匹配的位置;
    • 如果 s[i]t[j + 1] 相同,那么 ij 同时向后移动。
  • 如果 j 指向了模式串 t 的末尾,那么就说明模式串 t 完全匹配文本串 s 里的某个子串了。
int strStr(string haystack, string needle) {
	if (needle.size() == 0) {
	    return 0;
	}
	int next[needle.size()];
	getNext(next, needle);
	int j = -1; // // 因为next数组里记录的起始位置为-1
	for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始
	    while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配
	        j = next[j]; // j 寻找之前匹配的位置
	    }
	    if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动
	        j++; // i的增加在for循环里
	    }
	    if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t
	        return (i - needle.size() + 1);
	    }
	}
	return -1;
}

匹配过程如下:
在这里插入图片描述

时间复杂度: O ( n + m ) O(n + m) O(n+m)
空间复杂度: O ( m ) O(m) O(m), 只需要保存字符串 needle 的前缀表。

放弃一件事很容易,每天能坚持一件事一定很酷,一起每日一题吧!
关注我LeetCode主页 / CSDN—力扣专栏,每日更新!

注:仅供学习参考,如有不足,欢迎指正!

  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
KMP算法(Knuth-Morris-Pratt算法)是一种字符串匹配算法,用于在一个文本串S中查找一个模式串P的出现位置。它的核心思想是利用已经匹配过的部分字符,尽量减少不必要的比较次数。 KMP算法的步骤是这样的: 1. 预处理模式串P,得到一个next数组。next[i]表示P[0:i]这个子串中,最长的相等前缀和后缀的长度。我们可以通过不断比较P的前缀和后缀得到这个next数组。 2. 遍历文本串S,同时遍历模式串P。当遇到不匹配的字符时,根据next数组,将模式串P向右移动尽量少的距离。移动的距离由next数组中的值决定。 3. 当P移到最右端时,如果还是没有匹配成功,则继续将P向右移动一个位置,继续匹配。 这样,通过预处理模式串P,我们能够在匹配过程中尽量少的进行字符比较,提高了算法的效率。 终于全部弄懂了KMP算法,我明白了它的原理和实现过程。它的核心在于构建next数组,这个数组能够帮助我们在匹配过程中避免重复比较已经匹配过的字符。通过next数组,我们可以事先知道模式串P中的每个位置的最长相等前缀和后缀的长度,从而决定每次匹配过程中的移动距离。 KMP算法是一种高效的字符串匹配算法,它的时间复杂度为O(n+m),其中n和m分别是文本串S和模式串P的长度。相比于暴力匹配算法的时间复杂度O((n-m+1)m),KMP算法具有明显的优势。 了解KMP算法的原理和实现,对于我在日常编程中遇到的字符串匹配问题将会更加得心应手。我相信通过不断实践和总结,我能够更加熟练地运用KMP算法,解决字符串匹配相关的挑战。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

酷酷的懒虫

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值