数据结构与算法笔记:基础篇 -字符串匹配(下):如何借助BM算法轻松理解KMP算法?

概述

上篇文章讲了 BM 算法,尽管他复杂,也不好理解,但确实工程中非常好用的一种高效字符串匹配算法。有统计说,它是最搞笑、最常用的字符串匹配算法。不过,在所有的字符串匹配算法里,要说最知名的一种的话,那就非 KMP 算法莫属。很多时候,提到字符串匹配,我们首先想到的就是 KMP 算法。

尽管在实际的开发中,我们几乎不大可能自己亲手实现一个 KMP 算法。但是,学习这个算法的思想,作为你开拓眼界、锻炼下思维逻辑,也是极好的,所以有必要拿出来讲一讲。不过 KMP 算法是出了名的不好懂。我会尽力把它讲清楚,但是你也要多动动脑子。

实际上,KMP 算法跟 BM 算法的本质是一样的。上篇文章,我们讲了坏字符规则和好后缀规则,本章,我们就看下,如何记住上篇文章 BM 算法的讲解思路,让你能更好地理解 KMP 算法?


KMP 算法的基本原理

KMP 算法是根据三位作者(D.Kunth,J.H.Morris 和 V.R.Pratt)的名字来命名的,算法的全称是 Kunth Morris Pratt 算法,简称 KMP 算法。

KMP 算法的核心思想,跟 BM 算法非常相似。假设主串是 a,模式串是 b。在模式串与主串匹配的过程中,当遇到不可匹配的字符时,我们希望找到一些规律,可以将模式串往后多滑动 几位,跳过哪些肯定不会匹配的情况。

还记得上篇文章讲到的好后缀和坏字符吗?这里可以类比一下,在模式串和珠串匹配的过程中,把不能匹配的那个字符仍然叫做坏字符,把已经匹配的那段字符串叫做好前缀

在这里插入图片描述

当遇到坏字符时,我们就要把模式串往后滑动,在滑动的过程中,只要模式串和好前缀有上下重合,前面几个字符的比较,就相当于拿好前缀的后缀子串,跟模式串种的前缀子串在比较。这个比较过程能够更高效呢?可以不用一个字符一个字符的比较呢?

在这里插入图片描述

KMP 算法就是在视图寻找一种规律:在模式串和主串匹配的过程中,当遇到坏字符后,对于已经比对过的好前缀,能够找到一种规律,将模式串一次性滑动很多玩?

我们只需要拿好前缀本身,在它的后缀子串中,查找那个最长的可以跟好前缀的前缀子串匹配的。假设最长的可匹配的那部分前缀子串是 {v},长度是 k。我们把模式串一次性往后滑动 j-k 位,相当于,每次遇到坏字符时,我们就把 j 更新为 k,i 不变,然后继续比较。

在这里插入图片描述

为了表述方便,我把好前缀的所有后缀子串中,最长的可匹配前缀子串叫做最长可匹配后缀子串;对应的前缀子串叫做最长可匹配前缀子串

在这里插入图片描述

如何来求好前缀的最长可匹配前缀和后缀子串呢?我发现,这个其实不涉及主串,只需要通过模式串本身就能求解。所以,我就在想,能不能事先处理计算好,在模式串和主串匹配的过程中,直接拿来就用呢?

类似 BM 算法中的 bc、suffix、prefix 数组,KMP 算法也可以提前构建一个数组,用来存储模式串中每个前缀(这些前缀可能是好前缀)的最长可匹配前缀子串的结尾字符下标。我们把这个数组定义为 next 数组,很多书籍中还给这个数组起了一个名字,叫失效函数(failure function)。

数组的下标是每个前缀结尾字符 下标,数组的值是这个前缀的最长可匹配前缀子串的结尾字符下标。这句话有点拗口,举个例子,你一看应该就懂了。

在这里插入图片描述

有了 next 数组,我们很容易就可以实现 KMP 算法了。先假设 next 数组已经计算好了,先给出 kmp 算法的框架代码。

    // a、b分别是主串和模式串,n、m分别是主串长度和模式串长度public static int kmp(char[] a, int n, char[] b, int m) {int[] next = getNexts(b, m);int j = 0;for (int i = 0; i < n; i++) {while (j > 0 && a[i] != b[j]) { // a[i]!=b[j],表示找到换字符j = next[j - 1] + 1;}if (a[i] == b[j]) {j++;}if (j == m) { // 找到匹配模式串了return i-m+1;}}return -1;}

失效函数计算方法

KMP 算法的基本原理讲完了,现在来看下最复杂的部分,也就是 next 数组是如何计算出来的?

当然,我们可以用非常笨的方法,比如要计算下面这个模式串 bnext[4],我们就把 b[0,4] 的所有后缀子串,从长到短找出来,依次看看,是否能跟模式串的前缀子串匹配。很显然,这个方法也可以计算得到 next 数组,但是效率非常低。有没有更加高效的方法呢?

在这里插入图片描述

这里的处理非常有技巧,类似与动态规划。不过,动态规划我们在后面才讲到,所以,我们这里换种方法解释。

按照下标从小到大,依次计算 next 数组的值。当我们要计算 next[i] 的时候,前面的 next[0],next[1], ..., next[i-1] 已经计算出来了。利用已经计算出来的 next 值,是否可以快速推导出 next[i] 的值呢?

如果 next[i-1]=k-1,也就是说,子串 b[0,k-1]b[0,i-1] 的最长可匹配前缀子串。如果子串 b[0,k-1] 的下个字符 b[k],与 b[0,i-1] 的下一个字符 b[i] 匹配,那 b[0,k] 就是b[0,i] 的最长可匹配前缀子串。所以,next[i] 等于 k。但是,如果 b[0,k-1] 的下一个字符 b[k]b[0,i-1] 的下一个字符 b[i] 不相等呢?这个时候就不能简单地通过 next[i-1] 得到 next[i] 了。这个时候该怎么办呢?

在这里插入图片描述

我们假设 b[0,i] 的最长可匹配子串是 b[r,i] 。如果我们把最后一个字符去掉,那 b[r,i-1] 肯定是 b[0,i-1] 的可匹配后缀子串,但不一定是最长可匹配后缀子串。所以,既然 b[0,i-1] 最长可匹配后缀子串对应的模式串的前缀子串的下一个字符不等于 b[i],那么我们就可以考察 b[0,i-1] 的次长可匹配后缀子串 b[x,i-1] 对应的可匹配前缀子串 b[0,i-1-x] 的下个字符 b[i-x] 是否等于 b[i]。如果等于,那 b[x,i] 就是 b[0,i] 的最长可匹配后缀子串。

在这里插入图片描述
可是,如何求得 b[0,i-1] 的次长可匹配后缀子串呢?次长可匹配后置子串肯定被包含在最长可匹配后缀子串中,而最长可匹配后缀子串又对应最长可匹配前缀子串 b[0,y]。于是,查找 b[0,i-1] 的次长可匹配后缀子串,这个问题就变成查找 b[0,y] 的最长可匹配后缀子串的问题了。

在这里插入图片描述

按照,这个思路,我们可以考察完所有的 b[0,i-1] 的可匹配后缀子串 b[y,i-1],直到找到一个可匹配的后缀子串,它对应的前缀子串的下一个字符等于 b[i],那这个 b[y,i] 就是 b[0,i] 的最长可匹配后缀子串。

前面已经给出 KMP 算法的框架代码了,现在我把这部分的代码也写出来。这两部分代码合在一起,就是整个 KMP 算法的代码实现。

    // b表示模式串,m表示模式串长度private static int[] getNexts(char[] b, int m) {int[] next = new int[m];next[0] = -1;int k = -1;for (int i=1; i < m; i++) {while (k!=-1 && b[k+1] != b[i]) {k = next[k];}if (b[k+1] == b[i]) {k++;}next[i] = k;}return next;}

KMP 算法复杂度分析

KMP 算法的原理和实现讲完了,现在来分析一下 KMP 算法的时间、空间复杂度是多少?

空间复杂度很容易分析,KMP 算法只需要一个额外的 next 数组,数组大小跟模式串相同。所以空间复杂度是 O ( m ) O(m) O(m),m 表示模式串的长度。

KMP 算法包含两部分,第一部分是构建 next 数组,第二部分才是借助 next 数组匹配。所以,关于时间复杂度,我们要分别从这两部分来分析。

先分析第一部分的时间复杂度

计算 next 数组的代码中,第一层 for 循环中 i 从 1 到 m-1,也就是说,内部的代码被执行了 m-1 次。for 循环内部有一个 while 循序,如果我们能执行每次 for 循环、while 循序平均执行的次数,假设是 k,那时间复杂度就是 O ( k ∗ m ) O(k*m) O(km)。但是 while 循环执行的次数不怎好统计,所以我们放弃这个分析方法。

我们可以找一些参照变量,i 和 k。i 从 1 开始一直增加到 m,而 k 并不是每次 for 循环都会增加,所以,k 累积增加的值肯定小于 m。而 while 循环里 k=next[k],实际上是在减少 k 的值,k 累积都没有增加超过 m,所以 while循环里面 k=next[k] 总的执行次数不可能超过 m。因此,next 数组计算的时间复杂度是 O ( m ) O(m) O(m)

再分析第二部分的时间复杂度

分析方法和第一部分是类似的。 i 从 0 循序增长到 n-1,j 增长量不可能查过 i,所以肯定小于 n。而 while 循序中的那条语句 j=next[j-1]+1,不会让 j 增长的,那有没有可能让 j 不变呢?也没有可能。因为 next[j-1] 的值肯定小于 j-1,所以 while 循环中的这条语句实际上也是让 j 在减少。而 j 总共增长的量都不会超过 n,所以 while 循环中的这条语句总的执行次数也不会超过 n,所以这部分的时间复杂度是 O ( n ) O(n) O(n)

所以,综合两部分的时间复杂度,KMP 算法的时间复杂度是 O ( m + n ) O(m+n) O(m+n)

小结

KMP 算法讲完了,不知道你理解了没有?如果没有,建议多看几遍,自己多思考思考。KMP 算法和上篇文章的 BM 算法的本质非常类似,都是根据规律在遇到坏字符时,把模式串往后多滑动几位。

BM 算法有 2 个规则,坏字符和好后缀。KMP 算法借鉴 BM 算法的思想,可以总结出好前缀规则。这里最难懂的就是 next 数组的计算。如果用最笨的方法来计算,确实不难,但是效率会比较低。所以,我讲了一种类似动态规划的方法,按照下标 i 从小到大,依次计算 next[i],并且 next[i] 的计算通过前面已经计算出来的 next[0],next[1], ..., next[i-1] 来推导。

KMP 算法的时间复杂度是 O ( m + n ) O(m+n) O(m+n),不过它的分析过程稍微需要一点技巧,不那么直观,你只要看懂就好了,并不需要掌握,在我们平常的开发中,很少会有这么难分析的代码。

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

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

相关文章

【C++】认识STL

【C】认识STL STL的概念STL的版本STL的六大组件STL的三个境界STL的缺陷 STL的概念 SLT(standard template libaray-标准模板库)&#xff1a;是C标准库的重要组成部分&#xff0c;不仅是一个可复用的组件库&#xff0c;而且是一个保罗数据结构与算法的软件框架。 STL的版本 原…

便携式手持气象仪:低功耗设计

TH-LSZ05便携式手持气象仪是一款轻便、操作简便的气象监测工具&#xff0c;集成了风向、风速、大气压、温度、湿度五项气象要素的测量功能。这些设备通常设计为体积小、重量轻&#xff0c;以便于用户随时携带并使用。通过使用手持气象仪&#xff0c;用户可以实时获取关键的气象…

列表(list)(Python)

文章目录 一、定义二、列表常用操作 一、定义 list ["张三", "李四", "王五", "赵六"]二、列表常用操作 分类关键字/函数/方法说明增加列表.append(值)在列表末尾追加值列表.insert(索引&#xff0c; 值)在指定位置插入值&#xff…

安徽保安员精选模拟试题(含答案)

1、风险管理的三要素是()&#xff0c;风险评价和风险控制。 A、频率分析 B、风险分析 C、风险转移 D、后果估计 答案:B 2、治安保卫重要部位是指由()确定的、关系本单位生产业务全局的部位和生产环节。 A、企事业重点单位 B、地方政府 C、企事业单位保卫协会 D、公安机关 …

智能室内空气质量监测预警系统小程序设计说明书

智能室内空气质量监测预警系统小程序设计说明书 一、应用功能与系统设计 &#xff08;一&#xff09; 应用功能 该小程序设计的目的是为了配合环境监测吸顶灯,Mini空气监测仪等硬件设备实时数据展示与远程设备控制等功能&#xff0c;系统框架图如图1-1所示。用户可以从小程序…

【Qt 学习笔记】Qt系统相关 | Qt事件 | 事件的介绍及基本概念

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt系统相关 | Qt事件 | 事件的介绍及基本概念 文章编号&#xff1a;Qt…

BatchNormalization和Layer Normalization解析

Batch Normalization 是google团队2015年提出的&#xff0c;能够加速网络的收敛并提升准确率 1.Batch Normalization原理 图像预处理过程中通常会对图像进行标准化处理&#xff0c;能够加速网络的收敛&#xff0c;如下图所示&#xff0c;对于Conv1来说输入的就是满足某一分布…

一招解决家里粉尘螨虫太多难题?家用空气净化器哪款品牌效果好?

一到夏天&#xff0c;两天不打扫家里&#xff0c;家里就会布满一层粉尘。而且春夏的气候也是粉尘螨虫生长和繁殖疯狂时期&#xff0c;一不注意室内空气污染卫生的情况下&#xff0c;就会加剧尘螨的滋生&#xff0c;体质弱、敏感的人群生活在这样的空气环境下&#xff0c;还会增…

敏捷开发时代,彻底结束了

最近&#xff0c;我收到一位读者的私信&#xff0c;他最近“内耗”得非常厉害&#xff0c;他可能一时兴起把我的私信当作了吐槽箱。 他们公司一直实行敏捷的管理模式&#xff0c;复盘发现了一个问题&#xff1a;发布与迭代具有强相关性&#xff0c;一个迭代就发布一次&#xf…

Hadoop安装和测试

一&#xff0c;下载 地址&#xff1a;Index of /dist/hadoop/common 选择3.3.6版本&#xff08;最新版本之前的一个版本&#xff0c;一般比较稳定&#xff09; 二&#xff0c;解压 解压到/data/module目录&#xff0c;这里随便自定义就好。 tar -zxvf hadoop-3.3.6.tar.gz …

从《2024年人工智能指数报告》 看AI的最新发展趋势

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 《2024年人工智能指数报告》是由斯坦福大学的“以人为本”人工智能研究所&#xff08;Stanford HAI&#xff09;发布的&#xff0c;具体发布时间…

百货商场:打造品质生活

走进我们的百货商场&#xff0c;仿佛置身于一个五彩斑斓的梦幻世界。百货&#xff0c;不仅仅是购物的场所&#xff0c;更是一种品质生活的体验。 在这里&#xff0c;您可以找到最适合自己的商品选择。从家居用品到时尚服饰&#xff0c;从美食佳肴到美妆护肤&#xff0c;每一样商…

深入探索Java开发世界:Java基础~类型分析大揭秘

文章目录 一、基本数据类型二、封装类型三、类型转换四、集合类型五、并发类型 Java基础知识&#xff0c;类型知识点梳理~ 一、基本数据类型 Java的基本数据类型是语言的基础&#xff0c;它们直接存储在栈内存中&#xff0c;具有固定的大小和不变的行为。 八种基本数据类型的具…

Vue46-render函数

一、非单文件和单文件的main.js对比 1-1、非单文件的main.js 1-2、 单文件的main.js 将单文件的main.js中的render函数变成非单文件的main.js中的template形式&#xff0c;报如下错误&#xff1a; 解决方式&#xff1a; 二、解决方式 2-1、引入完成版的vue.js 精简版的vue&a…

推广结算统计,Xinstall助您轻松掌握每一分投入与回报!

在移动互联网时代&#xff0c;App的推广与运营离不开精准的数据支持和高效的结算系统。然而&#xff0c;面对众多的推广渠道和复杂的结算流程&#xff0c;如何确保每一分投入都能得到合理的回报&#xff0c;成为了众多企业和开发者关注的焦点。今天&#xff0c;我们就来聊聊如何…

半监督学习

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 介绍一、Self Training自训练1、介绍2、代码示例3、参数解释 二、Label Propagation&#xff08;标签传播&#xff09;1、介绍2、代码示例3、参数解释 三、Label Spread…

618狂欢日,美味产品齐上阵,超值优惠等你享

这个充满激情与活力的6月&#xff0c;我们带着满满的诚意与惊喜&#xff0c;为广大美食爱好者们开启一场独特的618狂欢之旅。 当我们提及甘肃&#xff0c;那丰富多样的甘肃传统美食便是不得不说的瑰宝。烤馍&#xff0c;油饼&#xff0c;锅盔、擀面皮、浆水等每一种美食都…

你知道花洒其实起源于中国古代吗?

花洒作为日常生活中不可或缺的一部分&#xff0c;其发展历程不仅见证了人类文明的进步&#xff0c;也反映了生活美学的演变。从最初的简单构想到现代的智能化设计&#xff0c;花洒的变迁历程是一部生动的人类生活史。 早在隋朝时期&#xff0c;我们的祖先就已经有了花洒的初步构…

《纪元 1800》好玩吗? 苹果电脑能玩《纪元 1800》吗?

《纪元1800》是一款不错的策略游戏&#xff0c;这款游戏因为画面和玩法独特深受玩家们的喜爱。下面我们来看看《纪元 1800》好玩吗&#xff0c;苹果电脑能玩《纪元 1800》吗的相关内容。 一、《纪元1800》好玩吗 《纪元1800》是一款备受瞩目的策略游戏。下面让我们来看看这款…

初探工厂抽象模式

设计模式的-工厂模式 1.定义一个约定的规则抽象类 class ETFactory {createStore() {throw new Error(抽象方法&#xff0c;不允许直接调用&#xff0c;需重写)}createUser(){throw new Error(抽象方法&#xff0c;不允许直接调用&#xff0c;需重写)} } 案例&#xff1a;…