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

news2024/10/5 15:29:13

概述

上篇文章讲了 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.coloradmin.cn/o/1831835.html

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

相关文章

【Linux命令行】从时间管理->文件查找压缩的指令详解

目录 1.date 命令&#xff08;显示时间&#xff09; 1.1 显示方面 1.2 设定时间 1.3 时间戳转换 1.4 cal&#xff08;日历&#xff09; 2. 重定向 2.1 输出重定向&#xff08;echo >&#xff09;cin 2.2 追加重定向 >> 2.3 输入重定向 < cout 3.find 指…

计算机网络-BGP路由优选原则六-优选MED属性值最小的路由

一、优选MED属性值最小的路由 BGP路由属性中的MED&#xff08;Multi-Exit Discriminator&#xff0c;多出口鉴别器&#xff09; 是一个可选的非传输属性&#xff0c;其设计目的是影响EBGP邻居的路由选择。MED是一个4字节的整数&#xff0c;其值的范围从0到4294967295。默认情况…

Elasticsearch:智能 RAG,获取周围分块(一)

作者&#xff1a;来自 Elastic Sunile Manjee 在检索增强生成 (RAG) 领域&#xff0c;一个持续存在的挑战是找到输入大型语言模型 (LLM) 的最佳数据量。数据太少会导致响应不足或不准确&#xff0c;而数据太多会导致答案模糊。这种微妙的平衡启发我开发了一个专注于智能分块和利…

超参数优化(网格搜索简介)

网格搜索简介 1、 网格搜索优化2、随机网格搜索 1、 网格搜索优化 在所有超参数优化&#xff08;HypeParameter Optimization&#xff0c;HPO&#xff09;中&#xff0c;枚举网格搜索&#xff08;GridSearchCV&#xff09;是最为基础和经典的方法 在搜索开始之前&#xff0c;我…

List实现类——ArrayList、LinkedList及迭代器(并发修改异常错误)源码分析

1、ArrayList本质及源码分析 两种情况&#xff1a;加一个和很多个 底层原理&#xff1a;elementData是底层数组的名字 再次满了的话&#xff0c;在扩容1.5倍 如果利用addAll一次添加多个元素&#xff0c;按实际元素数进行扩容 源码分析&#xff1a; 1、空参构造&#xff0c;…

【源码】Spring事务之事务失效及原理

Spring事务 1、【源码】SpringBoot事务注册原理 2、【源码】Spring Data JPA原理解析之事务注册原理 3、【源码】Spring Data JPA原理解析之事务执行原理 4、【源码】SpringBoot编程式事务使用及执行原理 5、【源码】Spring事务之传播特性的详解 6、【源码】Spring事务之…

语音识别相关文章整理目录

一、语音大模型架设与功能实现 使用sherpa-ncnn进行中文语音识别&#xff08;ubuntu22&#xff09;-CSDN博客文章浏览阅读953次&#xff0c;点赞30次&#xff0c;收藏26次。请注意&#xff0c;需要首先安装安装了所有必要的依赖项&#xff0c;包括 CMake、Git 和一个合适的 C/…

风控中的文本相似方法之余弦定理

一、余弦相似 一、 余弦相似概述 余弦相似性通过测量两个向量的夹角的余弦值来度量它们之间的相似性。0度角的余弦值是1&#xff0c;而其他任何角度的余弦值都不大于1&#xff1b;并且其最小值是-1。 从而两个向量之间的角度的余弦值确定两个向量是否大致指向相同的方向。结…

vite|webpack环境变量-多模式配置

vite官方文档&#xff1a;环境变量和模式 | Vite (vitejs.net) &#xff08;https://www.vitejs.net/guide/env-and-mode.html&#xff09; 一、工程根目录创建env文件如下&#xff1a; 文件中参数书写格式&#xff1a; VITE_XXXXXX XXXXXX。必须使用等号。必须以VITE_开头…

《地下城与勇士》新手攻略,开荒必备!云手机多开教程!

《地下城与勇士》&#xff08;DNF&#xff09;是一款广受欢迎的多人在线动作角色扮演游戏。玩家将在游戏中扮演不同职业的角色&#xff0c;通过打怪、做任务、PK等方式不断提升自己&#xff0c;探索广阔的阿拉德大陆。游戏中设有丰富的副本、装备、技能系统&#xff0c;玩家可以…

程序员画图工具?那必然是你了!!【送源码】

作为一个程序员&#xff0c;画图是必不可少的技巧。当然此画图不是搞艺术&#xff0c;而是画各种架构图、流程图、泳道图以及各种示意图。 平时我不论是记笔记、写技术文章&#xff0c;还是工作中写文档&#xff0c;都需要配上各种各样的示意图。不管是帮助自己更好的掌握知识…

「6.18福利」精选大厂真题|笔试刷题陪伴|明天正式开屋啦 - 打卡赢价值288元丰厚奖励

&#x1f370;关于清隆学长 大家好&#xff0c;我是清隆&#xff0c;拥有ACM区域赛 银牌&#x1f948;&#xff0c;CCCC天梯赛 国一&#xff0c;PTA甲级 98 分。 致力于算法竞赛和算法教育已有 3 年&#xff0c;曾多次 AK 互联网大厂笔试&#xff0c;大厂实习经验丰富。 打卡…

示例:WPF中使用DecodePixelHeight和DecodePixelWidth优化Image性能

一、目的&#xff1a;在使用Image控件时&#xff0c;如果图片太大或者图片数量过多时加载出来的程序内存会非常的大&#xff0c;但一般图片多时我们只要预览缩略图就可以&#xff0c;查看时再显示原图&#xff0c;这个时候需要通过通过设置BitmapImage的DecodePixelHeight和Dec…

Postgresql配置SSL连接

1、系统需要有openssl、openssl-devel包 yum -y install openssl openssl-devel 2、查看当前数据库是否使用openssl编译 pg_config|grep CONFIGURE 如果没有重新编译 make clean make && make install 3、服务器端证书配置 服务器端需生成三个文件: root.crt(根证…

浏览器调试小技巧

一. 使用XSwitch工具代理本地服务地址 1. 谷歌提供了一个扩展程序: XSwitch 工具描述: 一个重定向URL并允许CORS使本地开发体验轻松愉快的工具。 ps: 这个工具只有谷歌有, 只能翻墙后下载 安装成功后 长这样: 2. 全局安装http-server , 用于在本地启动一个服务 npm i http-…

白帽子最喜欢用什么渗透测试工具?看看哪些是你用过的

一、白帽子最喜欢用什么安全工具? 2020 年的 HackerOne 黑客报告中,统计过白帽子们最喜欢用的软硬件工具。 从图中可以看到,89% 的白帽子都会使用 Burp Suite 这个 Web 应用安全测试工具,有 39% 会尝试自己写工具,第三名的 Fuzzers 是模糊测试工具。再后面主要是一些代理…

Dart 弱引用进阶

前言 村里的老人说&#xff1a;“真正的强者&#xff0c;都是扮猪吃老虎。” 日常开发中经常需要用到弱引用&#xff0c;Dart 语言里也有提供弱引用的接口 WeakReference&#xff0c;我们可以基于它开发更强大的复杂结构。 在前面的文章中&#xff0c;我们用到了一个以弱引用…

现代易货:创新交易模式引领物品交换新潮流

在繁华的现代经济浪潮中&#xff0c;物品交换的文化逐渐崭露头角&#xff0c;引领了一种新颖的交易潮流——现代易货交易模式。这种模式不仅是对古老“以物易物”交易的现代化诠释&#xff0c;更是对物品价值多元化和交换方式创新的深入探索。那么&#xff0c;现代易货交易究竟…

螺丝工厂vtk ThreadFactory(1)

螺丝工厂vtkThreadFactory (1) 缘起 几年前的探索在Python里应用Openscad实现3D建模之3D螺纹建模初探3 新的参考: generating nice threads in openscadvtkRotationalExtrusionFilter 辅助AI: coze 笔记&#x1f4d2;: openscad 代码分析 // 半径缩放函数&#xff0c;用…

国货骄傲精亿内存条颠覆游戏战场,推出超强DDR5 7200玄武系列电竞内存

随着科技的迅猛发展,对高性能电脑的需求不断增长,特别是在电竞领域。认识到这一点,国货知名品牌精亿(JINGYI)推出了其全新一代DDR5 7200 RGB电竞内存条,并命名系列为象征中国上古四大神兽的玄武-系列。这款产品凭借其卓越性能和令人印象深刻的海力士A-DIE颗粒配置,正在迅速成为…