数据结构与算法一篇帮助你吃下KMP算法

模式匹配

  • 什么是模式匹配,我们用一个案例来说明:
    • 当S = “s1,s2,s3,s4 …sn” T=“t1,t2,t3,t4 … tn” 在字符串S中寻找T字符串的过程就是模式匹配的过程,T就说模式串,S是主串
  • 实现方案:
    • 暴力破解,逐字符判断,直到找到对应的全匹配
    • 由暴力破解的缺点逐步优化,引出KMP算法

暴力匹配

  • 思路如下:

    • 令i 是目标串S的下标, j是模式串T的下标
    • 当匹配到Si == Tj 的时候,我们做 i++,j++继续匹配下一个
    • 当匹配到Si != Tj 的时候,我们需要冲头匹配,另 i = i - j + 1, j = 0
    • 其中 i = i - j + 1 就说目标串S的回朔,我们匹配过j 个字符,那么回退 j个位置到当前这次匹配的最开始的位置,并且先前移动一位。
  • 根据如上分析有如下代码:

/*** Created by jiamin5 on 2022/2/12.* 给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回  -1 。* 来源:力扣(LeetCode第28题)* 链接:https://leetcode-cn.com/problems/implement-strstr*/
public class StrKmp {public static void main(String[] args) {System.out.println(strBM("asdfuohadsuoufhuuowyqruoqiwhgbyur23iu4y2oi34hf", "uoq"));}/*** 模式匹配BM暴力破解写法* */public static int strBM(String haystack, String needle){if (needle == null && haystack == null) {return 0;}if (haystack == null || haystack.length() <= 0) {return -1;}if (needle.isEmpty()) {return 0;}if (haystack.length() < needle.length()) {return -1;}char[] hasChar = haystack.toCharArray();char[] needChar = needle.toCharArray();int i = 0, j = 0;while (i<hasChar.length && j < needChar.length){if(hasChar[i] == needChar[j]){i++;j++;}else {i=i-j+1;j=0;}}if(j == needChar.length){return i-j;}return -1;}
}
暴力破解算法分析
  • 我们用一个特殊一点的案例来解释暴力破解算法
  • 依然用i 表示目标串S:ab abc abcac bab, 用j 表示模式串T:abcac, 有如下流程
  • 第一步,当Si, Tj 匹配到2 位置的时候,匹配是吧,i回朔到位置 2-2 +1 = 1,j=0
    在这里插入图片描述
  • 第二次匹配第一个字符就失败,j=1-0+1=2
    在这里插入图片描述
  • 第三次匹配,一直匹配到i= 6 的时候才匹配失败,这个时候,i=6-4+1 = 3,从第二个b开始匹配

在这里插入图片描述

  • 第四次从第二个b开始,显然是不可匹配到

在这里插入图片描述

  • 依次一直持续到第六次匹配,才匹配成功

在这里插入图片描述

  • 总结
    • 在如上流程中,每次匹配失败我们都回将i 回朔到当前匹配批次的下一个位置,j又从0开始比较
    • 在第三次匹配结束后,我们可以发现:i=3和j=0,i=4和j=0以及i=5和j=0是不必进行的
    • 关键点在于,我们在第三次匹配的时候,已经知道了前面的串中有ab abcab,从中我们可以得出,主串中第3,4,5个字符必然是‘b’,‘c’,‘a’,与模式串T中的 1,2,3 字符是相同的,是可以没必要在匹配的
    • 可以在第三步骤的时候,就知道,我们应该直接移动到i = 6 的位置,因为i = 6 的位置与j = 0 的位置都是 a 是想匹配的,之前的匹配过的字符已经是已知不可匹配的
    • 依照如上分析:如果有一个类似的数组,用来辅助挑转位置,那么明显会加快进程。这样就引出了我们的KMP算法,不回溯i,加快匹配效率。

KMP算法

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

  • 算法流程如下:

    • 如果j = -1 ,i++,j++,继续匹配下一个字符
    • 如果Si == Tj,i++, j++,继续匹配下一个字符
    • 如果Si != Tj,且j != -1,i不变,j = next[j],意味着在匹配失败的时候,接下来模式串T要相对于主串S向右移 j - next[j] 位置。
  • 那么会出现如下几个问题:

  1. next是什么
  2. 怎么得到next
位移信息next的确认
  • 要了解next的来源,先得知道一个名字:最长公共前后缀。假设有一个串P=“P0P1P2P3…Pj-1Pj”如果存在P0P1P2…Pk-1Pk = Pj-kPj-k+1…Pj-1Pj,我们就可以说在P串中最大长度为k+1 的公共前后缀
  • 找最大公共前后缀的意义:
    • 例如在意思的案例中:ab abcabcac bac,我们在第三步骤中匹配到第一个abc,到第六步骤中匹配到第二个abc,在中间四,五步骤都在确认该字符:abcab,其中他的最长公共前后缀长度是,也就是ab
    • 我们完全可以看出,单我们匹配到第一个abc的时候,完全可以直接跳过接下来的bc的匹配直接跳到第二个ab去匹配,那么最长公共前后缀就是这个作用,他告诉我们,在已经匹配过的字符串中,有没有这样的字符,我们直接跳过中间的字符,区匹配后面对于的字符ab就行了
前后缀确认
  • 还是用刚才那个案例字符串P = abaabca
    在这里插入图片描述

  • 这样公共先后在最长长度会和P的每个字符有一个对应关系:
    在这里插入图片描述

  • 我们从这个表引出next数组的值,next数组的值是除当前字符外的公共前后缀的长度,相当于将上表中数据向右移动一位,并且在前补充-1,得到如下
    在这里插入图片描述

  • 接下来我们通过next数组来进行匹配推到,如下图流程:

  • 第一步,i =0 ,j=0开始,当i = 2 ,j= 2 时候匹配失败,那么i不动,next[j]=next[2]=0,S右移j - next[j] = 2位 , j 回朔到 0
    在这里插入图片描述

  • 第二次匹配,i= 2开始,j=0, 当匹配到i= 6 j=4 时候,失败,还是一样 i不动 ,j = next[j]=next[4] = 1,也就是S向右移 j-next[j]=3位
    在这里插入图片描述

  • 第三次匹配,i = 6 ,j = 1 档i = 10, j = 5 时候匹配成功,返回 i-j=5
    在这里插入图片描述

结论:
  • 当主串S 与模式串P 匹配失败时候,j = next[j],也就是P 右移动j-next[j]。
  • 当模式串P后缀Pj-kPj-k+1…Pj-1与 主串S的Si-kSi-k+1…Si-1匹配成功,但是Pj,Sj j匹配失败时候,因为next[j] = k,相当于在不包含Pj的模式串中有最大长度为k的相同前后缀,也就是P0P1…Pk-1 = Pj-kPj-k+1…Pj-1
  • 所以我们令j = next[j],让模式串 右移 j - next[j]位,使模式串 P0P1…Pk-1与主串。Si-kSi-k+1…Si-1对齐,让Pk 与Si 继续匹配,如下图所示:
    在这里插入图片描述
代码实现求next数组
  • 之前我们是通过推到的方式得到的next数组,当我们用代码实现时候,需要有一定的规则

  • 当next[0] = -1,next[1] = 0这个容易得到

  • 那么当通用情况,当j > 1 时候,如果我们已知next[j], 那么next[j+1]的值是我们需要求解的

  • 有如下两种情况:

    • 当Pk = Pj,next[j+1] = next[j] + 1 = k + 1,代表字符E 前面的模式串中有长度为k+1 的最长公共前后缀
      在这里插入图片描述

    • 当Pk != Pj时候,说明p0p1…pk-1pk != pj-kpj-k+1…pj-1pj,这个时候ABC与ABD不相同,也就是E前面的模式串中没有长度为k+1的最大公共前后缀,所以next[j+1] = next[j] + 1 不在适用,我们需要寻找更短的最大公共前缀。
      在这里插入图片描述

    • 因为next数组存储的是之前字符中最长公共前后缀,那么当我们pk ! = pj 的时候,我们需要找k之前的一个匹配,那么就说next[k] 的位置就是上图中的D,我们需要匹配的是 Pnext[k] = Pj 是否成立,也就让k = next[k] 然后在做比较。如果成立,那么next[j+1] = next[next[k]]+ 1,如果不成了,next[j+1] = 0

  • 如上步骤中我们找k 之前的位置next[k]处的前缀来判断是否有更短前缀,那为什么我们将k = next[k]就能找到最短前后缀,如下图中所示

在这里插入图片描述

  • 如上图,在Pk!=Pj时候,k = next[k] 用 Pnext[k] 去根Pj 匹配,
  • 我们可以看到,Pj之前,有一段长度为k的已匹配串,在Pk之前也有一段蓝色的已匹配串,说明Pk字符前有一段长度为next[k] 的最大公共前后缀(蓝色标记)
  • 如果Pk != Pj,说明P0P1…Pk-1Pk != Pj-kPj-k+1…Pj-1Pj,那么我们需要向前着更短的最大公共前后缀
  • 因为此时Pk和Pnext[k] 前面的蓝色串已经完成了匹配
  • 如果Pnext[k]能和Pj匹配,那么我们就找到了最大公共前后缀,否则我们需要再次向前找 Pnext[next[k]] 去根Pj继续匹配,直到找到更短的最大公共前后缀,或者字符串已经找到最开始位置,那么长度就是 0

代码实现

  • 经过如上分析有如下代码:
/*** Created by jiamin5 on 2022/2/12.* 给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回  -1 。* 来源:力扣(LeetCode第28题)* 链接:https://leetcode-cn.com/problems/implement-strstr*/
public class StrKmp {public static void main(String[] args) {System.out.println(strKmp("asdfuohadsuoufhuuowyqruoqiwhgbyur23iu4y2oi34hf", "uoq"));}/*** 	KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。* 	KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。* 	具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。** */public static int strKmp(String haystack, String needle){if (needle == null && haystack == null) {return 0;}if (haystack == null || haystack.length() <= 0) {return -1;}if (needle.isEmpty()) {return 0;}if (haystack.length() < needle.length()) {return -1;}char[] hasChar = haystack.toCharArray();char[] needChar = needle.toCharArray();int [] next = getNext(needChar);int i = 0, j = 0;while (i<hasChar.length && j < needChar.length){if(j == -1 || hasChar[i] == needChar[j]){i++;j++;}else {j=next[j];}}if(j == needChar.length){return i-j;}return -1;}public static int[] getNext(char[] needChar){int[] next = new int[needChar.length+1];int j = 0, k = -1;next[j] = k;while (j < needChar.length){if(k == -1 || needChar[j] == needChar[k]){j++;k++;next[j] = k;}else {k = next[k];}}return next;}
}

参考文献

  • 大佬v_JULY_v 的文章,比我更详细

前期文章

  • 上一篇:数据结构与算法–求1~n能组成的所有二叉搜索树的排列
  • 下一篇:数据结构与算法–力扣108提将有序数组转换为二叉搜索树

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

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

相关文章

在远程 CSM 课程中体验线上工作坊

4.11 日周六&#xff0c;我参与了由 Bob 老师组织讲授的一期 Certified Scrum Master&#xff08;即 CSM&#xff09;课程&#xff0c;从中收获颇丰&#xff0c;特记于此&#xff0c;与君分享。CSM 通常是现场授课&#xff0c;但本次由于疫情的限制导致人们不得不尽可能减少外出…

数据结构与算法--力扣108题将有序数组转换为二叉搜索树

力扣108提将有序数组转换为二叉搜索树 近一年都比较关注算法相关的知识&#xff0c;也刷了不少题&#xff0c;之前的文章中大多也是算法相关的文章&#xff0c;但是感觉每次遇到树相关的题型都不能应对自如&#xff0c;因此还是有必要在相关知识上下功夫&#xff0c;因此有此次…

C#验证IP是否为局域网地址的三种方法

C#验证IP是否为局域网地址的三种方法前一阵子有【广州.NET群】的客户问起这个问题&#xff0c;说他们需要验证客户输入的网站是否为局域网。其实局域网的 IP并没有确定的定义&#xff0c;只要是局域网中&#xff0c;即可设置为任何一个 IP。但确实存在一个 内网保留地址的定义&…

数据结构与算法--力扣109题将有序双向链表转换为二叉搜索树

将有序数组转换为二叉搜索树 近一年都比较关注算法相关的知识&#xff0c;也刷了不少题&#xff0c;之前的文章中大多也是算法相关的文章&#xff0c;但是感觉每次遇到树相关的题型都不能应对自如&#xff0c;因此还是有必要在相关知识上下功夫&#xff0c;因此有此次总结&…

[Java基础]字符缓冲流

代码如下: package CopyJavaPack01;import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException;public class CopyJavaDemo04 {public static void main(String[] args) throws IOException {BufferedWriter bw new BufferedWriter(new FileWri…

在C#中使用RESTful API的几种好方法

在C&#xff03;中使用RESTful API的几种好方法原文来自互联网&#xff0c;由长沙DotNET技术社区编译。 通过Web开发的路径&#xff0c;您发现自己迟早需要处理外部API&#xff08;应用程序编程接口&#xff09;。在本文中&#xff0c;我的目标是列出在C&#xff03;项目中使用…

SpringBoot自动装配源码解析

Spring Boot 自动装配原理 使用Spring Boot最方便的一点体验在于我们可以几零配置的搭建一个Spring Web项目&#xff0c;那么他是怎么做到不通过配置来对Bean完成注入的呢。这就要归功于Spring Boot的自动装配实现&#xff0c;他也是Spring Boot中各个Starter的实现基础&#…

自制 .NET Core 路由调试中间件

点击上方蓝字关注“汪宇杰博客”导语本文教大家如何在 .NET Core 应用中使用中间件输出路由信息以便调试程序。背景在 .NET Framework 的上古时代&#xff0c;有个叫做 RouteDebugger 的神器&#xff0c;可以在 MVC 或 Web API 应用中输出当前页面的路由信息&#xff0c;也可查…

玉柴spn码故障对照表_后处理的故障不总是尿素泵故障,也有可能是这些原因

之前说到后处理故障&#xff0c;解决的都是柴油机尿素泵、喷嘴的&#xff0c;而SCR箱同样是一个重要的部件&#xff0c;它的作用就是将尿素液与尾气中的氮氧化物充分混合并发生化学反应的场所&#xff0c;目前重卡SCR箱集SCR催化器和发动机排气消声器与一体。整体材料为不锈钢&…

SpringBoot中Bean按条件装配

Conditional条件装配 Conditional是Spring Framework提供的一个核心功能注解&#xff0c;这个注解的作用是提供自动装配的条件限制&#xff0c;一般我们在用Configuration&#xff0c;Bean的时候使用它。也就是我们在自定义Bean的注入的时候&#xff0c;我们可以通过Condition…

定义一个手表_华米Amazfit Pop评测:一款功能全面的“性价比”手表

目前的智能手表虽然品牌、型号众多&#xff0c;但基本可以分为二种类型&#xff1a;第一种为入门级智能手表&#xff0c;其功能单一与智能手环差不多&#xff0c;但胜在屏幕大、能够带来更好的观感且价格便宜&#xff1b;第二种为旗舰级智能手表&#xff0c;功能全面、硬件水准…

[Java基础]复制文件的异常处理try...catch...finally的做法

代码如下: package ErrorOperatorPack;import java.io.FileReader; import java.io.FileWriter; import java.io.IOException;public class CopyFileDemo01 {public static void main(String[] args){}private static void method() {FileReader fr null;FileWriter fw null…

有哪些你踏入社会才明白的道理

不知不觉已经工作10多年&#xff0c;从一个懵懂的大学生到被社会无情毒打&#xff0c;终于成长一个职场老鸟。最近几天在胡思乱想&#xff0c;这10多年不少认知和感悟&#xff0c;如果10年前有人能告诉我&#xff0c;我会不会少走很多很多弯路&#xff1f;读书的时候&#xff0…

手把手教你 git revert merge

开发中git分支管理 研发流程 从develop分支切出一个新分支&#xff0c;根据是功能还是bug&#xff0c;命名为id-xxx 或 id-fixbug-*。开发者完成开发&#xff0c;提交分支到远程仓库。开发者发起merge请求&#xff0c;将新分支请求merge到develop分支&#xff0c;并提醒code r…

如何把自己的经历写成小说_古天乐的经历教会我们:如何在被欺骗以后改善自己的心理状态...

众所周知&#xff0c;这个只有太阳能黑他的男人&#xff0c;早年未发迹时曾干过泊车小弟等工作&#xff0c;后来作为模特经纪人接触娱乐圈&#xff0c;传闻某次模特迟到&#xff0c;古爷临时救场&#xff0c;算是正式踏入娱圈&#xff0c;出现在无数大牌歌星的MV里&#xff0c;…

从GC的SuppressFinalize方法带你深刻认识Finalize底层运行机制

如果你经常看开源项目的源码&#xff0c;你会发现很多Dispose方法中都有这么一句代码&#xff1a; GC.SuppressFinalize(this); &#xff0c;看过一两次可能无所谓&#xff0c;看多了就来了兴趣&#xff0c;这篇就跟大家聊一聊。一&#xff1a;背景1. 在哪发现的相信现在Mysql在…

NIO工作方式浅析

java Socket 工作机制 Socket是描述计算机之前相互通信的一种抽象功能。通过基于TCP/IP的流套接字协议建立连接A机器B机器通信—建立Socket连接—通过TCP连接&#xff08;端口号指定唯一应用&#xff09;----IP寻址&#xff08;寻找唯一主机&#xff09;----最终找到唯一主机上…