算法日记day 11(KMP算法)

news2024/11/13 12:34:20

一、KMP算法

基本原理:

KMP算法(Knuth-Morris-Pratt算法)是一种用于在一个文本串(字符串)中查找一个模式串(子串)的高效算法。它的主要优点是在匹配过程中避免了回溯(backtracking),从而提高了匹配的效率。在字符串匹配算法中,通过求解最长相等前后缀来解决不匹配的情况,利用前缀表记录每个位置的最长相等前后缀,这种方法称为KMP算法。

KMP算法的核心思想:

  1. 部分匹配表(Partial Match Table)

    • KMP算法的关键在于构建部分匹配表,也称为最长前缀匹配表(Longest Prefix which is also Suffix, LPS)。这张表记录了模式串中每个前缀的最长匹配前缀长度。
    • 通过部分匹配表,可以根据已经匹配的部分信息,快速调整模式串的位置,跳过不可能匹配的情况,从而避免不必要的比较。
  2. 匹配过程

    • 在匹配过程中,KMP算法会根据当前匹配的位置和部分匹配表来决定模式串的移动位置,以达到尽量减少匹配次数的目的。
    • 当匹配失败时,不会简单地将模式串向后移动一个位置,而是利用部分匹配表中的信息,将模式串移动到一个合适的位置再进行匹配,从而提高效率。

算法步骤概述:

  1. 构建部分匹配表

    • 根据模式串,构建部分匹配表,计算每个位置对应的最长匹配前缀长度。
  2. 匹配过程

    • 遍历文本串,同时根据部分匹配表调整模式串的位置,进行匹配。
    • 如果当前字符匹配成功,继续匹配下一个字符;如果匹配失败,根据部分匹配表移动模式串的位置,直到找到匹配或者完全匹配失败。

优点:

  • 避免回溯:KMP算法通过利用部分匹配表的信息,能够在模式串与文本串匹配过程中,减少不必要的比较,避免回溯到之前已经比较过的位置,从而提高了算法的效率。
  • 时间复杂度:KMP算法的时间复杂度为O(m + n),其中m为模式串长度,n为文本串长度,主要由构建部分匹配表和匹配过程决定。

那么如何用前缀表来进行匹配?什么是前缀表?前缀表如何求?

前缀表记录了模式串中每个位置的最长相等前后缀长度,使得在匹配失败时可以跳转到之前已匹配的位置继续匹配。这种方法相比于暴力匹配算法,时间复杂度从O(m*n)降低到了O(m+n)。

首先要理解什么是前缀,什么是后缀,前缀是包含首字母不包含尾字母的子串,后缀是包含尾字母不包含首字母的子串,以例子说明:

文本串:    "aabaabaaf"

模式串: "aabaaf"

KMP算法在匹配时,当第一次匹配后,文本串的"b"匹配模式串的"f"匹配失败,这是KMP算法的第二轮匹配会直接从模式串的"b"开始,匹配文本串的第二个"b",那么为什么是这样匹配的呢?为什么会直接从b开始匹配?这就是前缀表的功劳了。

对于模式串来说,它的前缀可以是 a,aa,aab,aaba,aabaa, 后缀可以是f,af,aaf,baaf,abaaf,正是遵循"最长相等前后缀的原则" ,以例子说明:

a的相等前后缀为0

aa的相等前后缀为1    (前缀a,后缀a)

aab的相等前后缀为0

aaba的相等前后缀为1   (前缀a,后缀a)

aabaa的相等前后缀为2     (前缀a,aa,后缀a,aa)

aabaaf的相等前后缀为0

最终得到的模式串前缀表为

(0,1,0,1,2,0)

 再我们第一轮找到不匹配的元素后,这时应该找前一位元素的最长相等前后缀是多少

文本串:    "aabaabaaf"

模式串: "aabaaf"

 前缀表: 010120

模式串中"f"的前一位是a,它的最长相等前后缀是2,那么这个2是什么意思?

它代表着子串"aabaa"中的一个两位后缀aa,同时也有着一个相等的两位前缀aa,由于匹配的后缀不相等造成冲突,因此需要去寻找与其相等的前缀,而前缀表中的这个2,就是下一次匹配时模式串的起始位置(下标), 2是相等前后缀的长度,现在已知了前缀,因此要从该相等前缀的下一个元素开始下一次串匹配。

 

这时第二次匹配为

 

文本串:    "aabaa  baaf"

模式串:       "aa  baaf"

 

这时匹配完成,KMP算法结束

总结:

KMP算法通过利用部分匹配表,避免了回溯,提高了模式串匹配的效率,特别适合于需要多次匹配模式串的场景,如字符串匹配、编辑距离计算等。

具体题目:

1、KMP算法经典应用

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

示例 1:

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

示例 2:

输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。

代码:

首先创建next数组作为字符串的前缀表(前缀表的值统一减一)

public void getNext(int[] next, String s){
    int j = -1;             // 初始化 j 为 -1,表示当前没有匹配的前缀
    next[0] = j;            // 第一个位置的部分匹配值为 -1
    for (int i = 1; i < s.length(); i++){  // 从第二个字符开始遍历 s 字符串
        while(j >= 0 && s.charAt(i) != s.charAt(j + 1)){
            // 如果当前字符不匹配,并且 j 大于等于 0,则通过 next[j] 回溯 j 的值
            j = next[j];
        }

        if(s.charAt(i) == s.charAt(j + 1)){
            // 如果当前字符匹配,则 j 向前移动一位
            j++;
        }
        next[i] = j;         // 记录当前位置 i 处的最长匹配前缀的末尾字符在模式串中的位置
    }
}
  1. 初始化

    • j = -1:初始化 j 为 -1,表示当前没有匹配的前缀。
    • next[0] = j:设置 next 数组的第一个位置为 -1,因为第一个字符没有前缀。
  2. 循环计算 next 数组

    • for (int i = 1; i < s.length(); i++):从字符串 s 的第二个字符开始遍历到末尾。
    • while(j >= 0 && s.charAt(i) != s.charAt(j + 1))
      • 如果当前字符不匹配,并且 j 大于等于 0,则通过 next[j] 回溯 j 的值,直到找到一个可以继续匹配的位置或者 j 回溯到 -1
    • if(s.charAt(i) == s.charAt(j + 1))
      • 如果当前字符匹配,则将 j 向前移动一位 j++
    • next[i] = j
      • 将 j 的值赋给 next[i],即记录当前位置 i 处的最长匹配前缀的末尾字符在模式串中的位置。

 

 KMP算法

public int strStr(String haystack, String needle) {
    if (needle.length() == 0) {
        return 0;   // 如果 needle 为空字符串,则返回 0
    }

    int[] next = new int[needle.length()];  // 创建 next 数组,用于存储部分匹配表
    getNext(next, needle);  // 调用 getNext 方法,计算 needle 的部分匹配表

    int j = -1;  // 初始化 j 为 -1,表示当前没有匹配的前缀
    for (int i = 0; i < haystack.length(); i++) {
        while (j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)) {
            // 如果当前字符不匹配,并且 j 大于等于 0,则通过 next[j] 回溯 j 的值
            j = next[j];
        }
        if (haystack.charAt(i) == needle.charAt(j + 1)) {
            // 如果当前字符匹配,则 j 向前移动一位
            j++;
        }
        if (j == needle.length() - 1) {
            return (i - needle.length() + 1);  // 如果 j 移动到 needle 的末尾,则找到匹配的起始位置
        }
    }
    return -1;  // 没有找到匹配的子串,返回 -1
}
  1. 参数和边界情况处理

    • strStr 方法接受两个字符串参数 haystack 和 needle,其中 haystack 是待搜索的主字符串,needle 是要搜索的子字符串。
    • 如果 needle 的长度为 0,直接返回 0,因为一个空的 needle 在任何位置都是匹配的起始位置。
  2. 初始化 next 数组

    • 创建一个 next 数组,用于存储 needle 字符串的部分匹配表。
  3. 计算部分匹配表

    • 调用 getNext(next, needle) 方法,这个方法计算并填充 next 数组,用于在匹配失败时快速确定下一次比较的位置。
  4. 主匹配循环

    • 使用两个指针 i 和 j 分别在 haystack 和 needle 中进行遍历和比较。
    • j 初始为 -1,表示当前没有匹配的前缀。
    • 遍历 haystack 中的每个字符,如果当前字符不匹配,并且 j 大于等于 0,则通过 next[j] 回溯 j 的值,直到找到一个可以继续匹配的位置或者 j 回溯到 -1
    • 如果当前字符匹配,则将 j 向前移动一位。
    • 如果 j 移动到 needle 的末尾 needle.length() - 1,则说明找到了完整的匹配子串,返回起始位置 (i - needle.length() + 1)
  5. 返回结果

    • 如果循环结束都没有找到匹配的子串,则返回 -1 表示未找到。

在java中提供有直接寻找子字符串的方法indexOf,如

public int strStr(String haystack, String needle) {
    return haystack.indexOf(needle);
}
  • indexOf 方法是 Java 字符串类 String 的方法,用于查找一个子字符串在原字符串中的位置。它从原字符串的开头开始,逐个位置尝试匹配子字符串,直到找到匹配或者遍历完整个字符串。
  • 如果找到了匹配的子字符串 needle,则返回第一次出现的索引位置。
  • 如果没有找到匹配的子字符串,则返回 -1

这段代码简单而有效地利用了 Java 提供的字符串方法来实现字符串搜索功能。它适用于大多数基本的字符串搜索需求,但需要注意的是,它并没有利用更高级的字符串匹配算法(如 KMP 算法)来提高效率,因此在大数据量或者复杂匹配模式的情况下,可能性能会受到影响。

 

2、重复的子字符串

题目:

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

示例 1:

输入: s = "abab"
输出: true
解释: 可由子串 "ab" 重复两次构成。

示例 2:

输入: s = "aba"
输出: false

示例 3:

输入: s = "abcabcabcabc"
输出: true
解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)

思路:

这里仍采用KMP算法,在最长相等前后缀中,由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串

 这是为什么呢?如何寻找是关键

 

步骤一:因为 这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,所以 s[0] 一定和 s[2]相同,s[1] 一定和 s[3]相同,即:,s[0]s[1]与s[2]s[3]相同 。

步骤二: 因为在同一个字符串位置,所以 t[2] 与 k[0]相同,t[3] 与 k[1]相同。

步骤三: 因为 这是相等的前缀和后缀,t[2] 与 k[2]相同 ,t[3]与k[3] 相同,所以,s[2]一定和s[4]相同,s[3]一定和s[5]相同,即:s[2]s[3] 与 s[4]s[5]相同。

步骤四:循环往复。

所以字符串s,s[0]s[1]与s[2]s[3]相同, s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同。

正是因为 最长相等前后缀的规则,当一个字符串由重复子串组成的,最长相等前后缀不包含的子串就是最小重复子串。

此时数组对应的next数组的值为:

数组:a b a b a b a b

next[]: 0 0 1 2 3 4 5 6

 再比如:

判断子字符串是否重复,只需要用数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环

//创建next数组
for (int i = 2, j = 0; i <= len; i++) {
    // 匹配不成功,j回到前一位置 next 数组所对应的值
    while (j > 0 && chars[i] != chars[j + 1]) j = next[j];
    // 匹配成功,j往后移
    if (chars[i] == chars[j + 1]) j++;
    // 更新 next 数组的值
    next[i] = j;
}

 判断是否为重复子字符串

if (next[len] > 0 && len % (len - next[len]) == 0) {
    return true;
}

 完整代码如下:

public boolean repeatedSubstringPattern(String s) {
    if (s.equals("")) return false;

    int len = s.length();
    // 原串加个空格(哨兵),使下标从1开始,这样j从0开始,也不用初始化了
    s = " " + s;
    char[] chars = s.toCharArray();
    int[] next = new int[len + 1];

    // 构造 next 数组过程,j从0开始(空格),i从2开始
    for (int i = 2, j = 0; i <= len; i++) {
        // 匹配不成功,j回到前一位置 next 数组所对应的值
        while (j > 0 && chars[i] != chars[j + 1]) j = next[j];
        // 匹配成功,j往后移
        if (chars[i] == chars[j + 1]) j++;
        // 更新 next 数组的值
        next[i] = j;
    }

    // 最后判断是否是重复的子字符串,这里 next[len] 即代表next数组末尾的值
    if (next[len] > 0 && len % (len - next[len]) == 0) {
        return true;
    }
    return false;
}

 

  1. 边界条件

    • if (s.equals("")) return false; 如果输入的字符串 s 是空字符串,则直接返回 false,因为空字符串不可能由重复的子字符串构成。
  2. 初始化

    • int len = s.length(); 获取字符串 s 的长度。
    • s = " " + s; 在字符串 s 的开头加上一个空格作为哨兵,使得字符串的索引从 1 开始,这样方便后续算法中的处理。
  3. 构造 next 数组

    • char[] chars = s.toCharArray(); 将字符串 s 转换为字符数组 chars,便于随后的字符操作。
    • int[] next = new int[len + 1]; 创建 next 数组,用于存储部分匹配表。
  4. KMP 算法核心部分

    • for (int i = 2, j = 0; i <= len; i++) { ... } 使用 i 和 j 两个指针,其中 i 表示当前正在匹配的位置,j 表示前缀的匹配长度。
    • while (j > 0 && chars[i] != chars[j + 1]) j = next[j]; 如果当前字符不匹配,并且 j 大于 0,则通过 next[j] 回溯 j 的值,直到找到一个可以继续匹配的位置。
    • if (chars[i] == chars[j + 1]) j++; 如果当前字符匹配,则将 j 向后移动一位。
    • next[i] = j; 更新 next 数组的值,表示当前位置的最长相同前缀后缀长度。
  5. 判断重复子字符串

    • if (next[len] > 0 && len % (len - next[len]) == 0) { return true; } 
      • next[len] 表示整个字符串的最长相同前缀后缀长度。
      • len % (len - next[len]) == 0 表示字符串长度能整除最长前缀后缀的长度,即可以由子字符串重复构成。
      • 如果上述条件满足,则返回 true,否则返回 false

 相关资料来源:

https://www.programmercarl.com/0459.%E9%87%8D%E5%A4%8D%E7%9A%84%E5%AD%90%E5%AD%97%E7%AC%A6%E4%B8%B2.html#%E6%80%9D%E8%B7%AF

https://leetcode.cn/problems/repeated-substring-pattern/

 今天的学习就到这里了

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

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

相关文章

【QT】label中添加QImage图片并旋转(水平翻转、垂直翻转、顺时针旋转、逆时针旋转)

目录 0.简介 1.详细代码及解释 1&#xff09;原label显示在界面上 2&#xff09;水平翻转 3&#xff09;垂直翻转 4&#xff09;顺时针旋转45度 5&#xff09;逆时针旋转 0.简介 环境&#xff1a;windows11 QtCreator 背景&#xff1a;demo&#xff0c;父类为QWidget&a…

盒须图boxplot 展示第6条线

正常情况下&#xff0c;盒须图是有5条线的&#xff0c;但是实际产品场景是需要6条线&#xff0c;看了下echarts官网&#xff0c;没看到可配置的地方&#xff0c;只能自己骚操作了&#xff0c;效果图如下&#xff1a; 重点&#xff1a;用两条x轴&#xff0c;第6条线挂在第二条x…

【flink】之如何快速搭建一个flink项目

1.通过命令快速生成一个flink项目 curl https://flink.apache.org/q/quickstart.sh | bash -s 1.19.1 生成文件目录&#xff1a; 其中pom文件包好我们所需要的基础flink相关依赖 2.测试 public class DataStreamJob {public static void main(String[] args) throws Except…

GitHub 令牌泄漏, Python 核心资源库面临潜在攻击

TheHackerNews网站消息&#xff0c;软件供应链安全公司 JFrog 的网络安全研究人员称&#xff0c;他们发现了一个意外泄露的 GitHub 令牌&#xff0c;可授予 Python 语言 GitHub 存储库、Python 软件包索引&#xff08;PyPI&#xff09;和 Python 软件基金会&#xff08;PSF&…

《昇思25天学习打卡营第25天|onereal》

初学入门/初学教程/08-模型训练.ipynb 模型训练 模型训练一般分为四个步骤&#xff1a; 构建数据集。定义神经网络模型。定义超参、损失函数及优化器。输入数据集进行训练与评估。 现在我们有了数据集和模型后&#xff0c;可以进行模型的训练与评估。 构建数据集 首先从数…

国内AI算力芯片厂商群雄逐鹿,创新产品引领边缘计算新风尚

近年来&#xff0c;随着人工智能技术的飞速发展&#xff0c;AI算力芯片作为支撑这一技术的关键基础设施&#xff0c;受到了前所未有的关注。国内厂商纷纷加入竞争&#xff0c;四川万物纵横科技就是其中争流涌进的一员&#xff0c;公司不仅在技术上追求创新&#xff0c;还在市场…

FastAPI 学习之路(五十二)WebSockets(八)接受/发送json格式消息

前面我们发送的大多数都是text类型的消息&#xff0c;对于text消息来说&#xff0c;后端处理出来要麻烦的多&#xff0c;那么我们可以不可以传递json格式的数据&#xff0c;对于前后端来说都比较友好&#xff0c;答案是肯定的&#xff0c;我们需要做下处理。 首先&#xff0c;…

LNMP环境配置问题整理

首先是一键安装直接报错&#xff1a; 换教程&#xff1a;搭建LNMP&#xff0c;步骤最详细&#xff0c;附源码&#xff0c;学不会打我-CSDN博客 mysql安装成功之后&#xff1a; MySQL 启动报错&#xff1a;Job for mysqld.service failed because the control process exited …

Java 在PDF中替换文字(详解)

目录 使用工具 Java在PDF中替换特定文字的所有实例 Java在PDF中替换特定文字的第一个实例 Java在PDF中使用正则表达式替换特定文字 其他替换条件设置 可能出现的问题及解决方案 PDF文档中的信息随时间的推移可能会发生变化&#xff0c;比如产品价格、联系方式等。为了确保…

迭代学习笔记

一、迭代学习定义和分类 1、直观理解 迭代学习一般应用于重复性的场景。比如控制一个单自由度的小车以特定的速度曲线移动到指定位置&#xff0c;整个时间是10s&#xff0c;控制频率是0.01&#xff0c;那么整个控制序列就会有1000个点。这1000个点在10s内依次发出&#xff0c…

网络编程-TCP 协议的三次握手和四次挥手做了什么

TCP 协议概述 1. TCP 协议简介 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是一种面向连接的、可靠的、基于字节流的传输层通信协议。 TCP 协议提供可靠的通信服务&#xff0c;通过校验和、序列号、确认应答、重传等机制保证数据传输…

Pytorch学习笔记day4——训练mnist数据集和初步研读

该来的还是来了hhhhhhhhhh&#xff0c;基本上机器学习的初学者都躲不开这个例子。开源&#xff0c;数据质量高&#xff0c;数据尺寸整齐&#xff0c;问题简单&#xff0c;实在太适合初学者食用了。 今天把代码跑通&#xff0c;趁着周末好好的琢磨一下里面的各种细节。 代码实…

C++内存管理(区别C语言)深度对比

欢迎来到我的Blog&#xff0c;点击关注哦&#x1f495; 前言 前面已经介绍了类和对象&#xff0c;对C面向对象编程已经有了全面认识&#xff0c;接下来要学习对语言学习比较重要的是对内存的管理。 一、内存的分区 代码区&#xff1a;存放程序的机器指令&#xff0c;通常是可…

js实现数组的下标为n的对象后面新增一条对象

前言&#xff1a; js实现数组的下标为n的对象后面新增一条对象 实现方法&#xff1a; arr.splice(1, 0, obj); splice 参数1: 数组里面的第几个元素&#xff0c;你希望在第几个对象后面新增参数2: 0 表示不删除任何元素参数3: 插入的新对象 let arr [{},{},{},{}] let obj…

vue使用echarts开发大屏可视化(附echarts案例资源)

近年来&#xff0c;可视化在前端领域是越来越多。最近投入的一个项目就是关于大屏可视化&#xff0c;基本就是用到了echarts&#xff0c;所以项目结束后&#xff0c;我也来总结一下如何在Vue中去引入echarts并使用。 文章目录 一、echarts案例网站可视化社区(https://www.makea…

Zoho Mail企业邮箱好用吗?

企业在选择企业邮箱时需要考虑三大因素&#xff0c;一是安全隐私&#xff0c;二是功能易用&#xff0c;三是产品价格。作为国际排行前五的企业邮箱&#xff0c;Zoho邮箱好用吗&#xff1f;本文将为您详细介绍Zoho邮箱的功能、安全性和产品价格。 一、安全隐私 1、数据加密与安…

MySQL----初始数据类型

前言 一、tinyint 范围&#xff1a;-128-----127 在MySQL中&#xff0c;整型可以指定是有符号的和无符号的&#xff0c;默认是有符号的。可以通过UNSIGNED来说明某个字段是无符号的。如果我们向mysqlt特定的类型中插入不合法的数据&#xff0c;Mysq一般会直接拦截&#xff0c…

【HarmonyOS学习】定位相关知识(Locationkit)

简介 LocationKit提供了定位服务、地理围栏、地理编码、逆地理编码和国家码等功能。 可以实现点击获取用户位置信息、持续获取位置信息和区域进出监控等多项功能。 需要注意&#xff0c;需要确定用户已经开启定位信息&#xff0c;一下的代码没有做这一步的操作&#xff0c;默…

p17面试题

品茗面试题 1.交换两个int变量的值&#xff0c;不能使用第三个变量&#xff0c;即a3,b5,交换后&#xff0c;a5,b3&#xff1b; #include<stdio.h> //int main(){ // //打印函数&#xff0c;引用头文件.stdio.h // printf("hello world\n");//打印函数 …

C++STL详解(二)——string类的模拟实现

首先&#xff0c;我们为了防止命名冲突&#xff0c;我们需要在自己的命名空间内实现string类。 一.string类基本结构 string类的基本结构和顺序表是相似的&#xff0c;结构如下&#xff1a; //.h namespace kuzi {class string{private:char* _str;//字符串size_t _size;//长…