数据结构(邓俊辉)学习笔记】串 17——Karp-Rabin算法:散列

embedded/2024/10/19 2:26:28/

文章目录

  • 1.数位溢出
  • 2.散列压缩
  • 3.应对冲突
  • 4.指纹更新

1.数位溢出

在这里插入图片描述

在前一节中,已经成功地完成了一次视角转换,了解到应该如何从数学上将每一个串视作为一个自然数。以下我们就来将这一构思具体的兑现为一个算法。很有意思的是,我们在此需要用到第9章的关键技术散列

我们将每一个串所对应的自然数称作为它的指纹 fingerprint。因这个数相对于串,就像指纹相对于人一样,可以用来甄别其身份。然而我们注意到,这样一个自然数是以字符集的规模作为进制的,因此字符集只要不是很小,这类指纹的数值就会变得很大,这不能不说是个坏消息。

比如,我们知道对于 ASCII 字符集来说,它的规模为128,对于这类字符,即便模式串的长度不是特别的长,它对应的指纹也会长的令我们吃不消。我们可以来做一个快速的封底估算,128 是 2 的7 次方,因此即便是长度为 10 的模式串,它所对应的指纹也至少需要70个比特方能表示。
~  
这就意味着,即便在64位的计算平台上,长度不小于10的字符串将无法直接表示。而更糟糕的是,我们因此而遇到的麻烦还不止这些。

实际上,在整数的字宽已经不能继续视作为常数之后,整数之间的运算,也不能继续保证可在常数时间内完成。尽管 RAM 模型曾经的确做过这样一个不切实际的假设。实际上就渐进复难度的意义而言,此时,每次指纹比对所需要的时间将仍然线性正比于串的长度,也就是说,我们计算效率将重新退化到蛮力算法的水准。

那么,为了破解这一难题,你又能想到哪些高招呢?

2.散列压缩

在这里插入图片描述

既然以上问题的根源在于数位的溢出,那么我们很自然的就会应该想到通过压缩来解决它。没错,将一个硕大的取值空间压缩到一个可存储、可计算的更小的空间。从方法论上讲,这也不正是散列吗?

没错,我们需要对指纹来做散列压缩。具体地,我们将借助合适的散列函数将字符串的指纹压缩到存储器可支持的范围

这里我们不妨就以模余法为例,模余的基底取作素数 97。接下来,假设我们需要在这样一个文本串中寻找模式串 82818, 以下的主体流程与蛮力算法一样:

  1. 我们也是从首个对齐位置开始,逐次地去尝试各个位置,在每一个位置我们都将局部的子串与模式串进行比对。

    而在这里最为本质的不同在于,我们将不再是逐个地去比较每个字符,而是直接在两个串的指纹之间进行比对。

  2. 为此我们首先要计算出模式串的指纹,如果没有算错,应该是77。在文本串中,我们首先尝试的是子串 27182。也不难验证,它所对应的指纹为22,与目标指纹77不符,因此我们可以随即将其排除并接着转向下一个字串,也就是71828。

  3. 同样地,也不难验证,它的指纹为48。与77不符,所以也同样被排除掉。

  4. 再接下来对应的字串为 18281,它所对应的指纹也不难验算为45,同样与77不符,因而可以排除掉。

  5. 不难看出,整个算法将在下一个对齐位置发现匹配。是的,这个子串与模式串完全一致,所以它所对应的指纹也应该为77,而实际上这也是算法所发现的。

于是通过这种方法,我们也同样地完成了一次模式匹配。需要提醒你再次注意的是,在整个这样的计算过程中,我们分别只需要常数的时间就可以排除或者确认一个对齐位置,而这一点正是我们设计这种算法的初衷

当然,算法至此,依然没有尽善尽美,你能看出其中的问题吗?

3.应对冲突

在这里插入图片描述

没错,冲突。既然是散列,冲突就必然不可避免。

不过好在,我们这里只用到了指纹相等置于匹配的必要性,这与散列中的试探过程完全同理

你应该记得在试探查找的过程中,即便我们发现对应的桶非空,我们也不会贸然地认为它必定就是我们要找的元素。是的,为了最终确定它的身份,我们还需要做一次严格的比对。而在这里,我们也不妨沿用这种方法。

来看这样一个实例:依然是刚才我们已经熟知的那个文本串,只不过这里将模式串替换为18284。

  1. 首先确认,模式串的指纹为48。

    以下,我们依然是去逐个尝试每一个对齐位置。

  2. 在第一个对齐位置,我们得到的是指纹 22,既然它与目标的 48 不等,我们也可自然地将这一对齐位置排除掉。

  3. 接下来的第二个对齐位置,所对应的局部子串是 71828。很显然,它并不是我们要找的模式串。然而很不幸,它所对应的指纹却是 48,与模式串的指纹一模一样。当然,这也没有什么奇怪的,两个不同的元素在经过散列变换之后,有可能会被映射到同一个散列码,这种现象正是我们所谓的冲突。

    好在,我们沿用了散列表的策略,我们在这种情况下还会对这两个字符串做逐位的比对,以最终确定它的确是匹配。当然经过这样的严格比对之后,我们也的确可以排除掉这个对齐位置。

事实上,这个算法会如此不断地继续运行下去,直到最终抵达真正的匹配穿,也就是18284。当然,我们算法在此也不会遗漏掉这次匹配,因为显然这个局部子串所对应的散列码也应该是48。

这样我们又再次向前迈出了坚实的一步,至此,我们距离 Karp-Rabin 算法只差最终的一小步了。

4.指纹更新

在这里插入图片描述

就目前的模式而言,为了计算出文本串中每一个子串所对应的指纹,我们所需要花费的时间似乎都需线性正比于子串的长度

当然也就是模式串的长度,如果考虑到有多达 O(n) 个这样的潜在子串,那么你或许会沮丧的发现,最终的整体时间复杂度又再一次回到了O(n) * m,难道,我们此前的心血都是白费的吗?这也是我们在最后这一小步中所要解决的一个关键问题。

我们思考的方向和目标是,将每一指纹的计算成本从 O(m) 降低到常数。如果你还没有想出有效的方法,不妨首先温习一下此前的进制转换算法,或许你能从中得到一些启示。

没错,在那个算法中,我们很好地利用了相邻数位之间的相关性。其实在这里,也存在着类似的相关性。在文本串中,任何两个相邻子串之间都存在着紧密的相关性。具体来说,二者的指纹之间存在的相关性,而这两个指纹的计算过程以及结果之间也存在着紧密的相关性。

从这幅图中可以看出,相邻的子串几乎一样,二者唯一的区别只在于前者的首字符以及后者的末字符。只要能够悟到这一点,相信你就不难设计出一种新的方法,使得我们可以在常数的时间内由前一个指纹得到后一个指纹。


http://www.ppmy.cn/embedded/103112.html

相关文章

【Java设计模式】转换器模式:简化跨层数据转换

文章目录 【Java设计模式】转换器模式:简化跨层数据转换一、概述二、转换器设计模式的别名三、转换器设计模式的意图四、转换器模式的详细解释及实际示例五、Java中转换器模式的编程示例六、何时在Java中使用转换器模式七、转换器模式在Java中的实际应用八、转换器模…

AI人像换脸!Reactor插件本地部署方法(含报错解决及整合包)

​ Reactor插件是什么?有什么用? Reactor 是一个用于 Stable Diffusion 的换脸插件, 主要功能是实现图片中的精确换脸。它可以自动检测并替换图片中的多个面部,适用于多种场景,比如生成逼真的图像或者进行复杂的图片处…

日志文件log4j

今天在处理一个文件,不打印日志的情况,刚好其他项目是logback,这个项目是log4j。另外这个项目中的logback.xml未生效。应该和pom文件有一点关系。不过懒得改了。能用就行。 log4j的DailyRollingFileAppender,在当天不会显示出来。…

【Java设计模式】组合视图模式:增强应用程序中UI的一致性

【Java设计模式】组合视图模式:增强应用程序中UI的一致性 一、概述 在Java中,组合视图设计模式有助于管理复杂的层次视图。本文将详细介绍该模式的意图、解释、编程示例、适用场景、实际应用、优点和权衡。同时,还将提供示例代码的下载链接…

Springboot里集成Mybatis-plus、ClickHouse

🌹作者主页:青花锁 🌹简介:Java领域优质创作者🏆、Java微服务架构公号作者😄 🌹简历模板、学习资料、面试题库、技术互助 🌹文末获取联系方式 📝 Springboot里集成Mybati…

android studio 新建java工程, 安卓新建项目,android studio2024 如何新建java项目

主要解决,新增安卓工程,没有java选项 1. 点击左上角FIle -> New -> 2. 选择 no activity 选项, 然后next 3. langua 就可以选择java 了。name自己定义项目名称,项目存储地址,包名。 配置完成选择finish. 4. fin…

AWS 中的信任策略的危险

介绍 使用过 Amazon Web Services (AWS) 的每个人都知道,云环境有一种独特的方式来授予用户和资源的访问权限。这是通过允许用户和/或资源临时承担角色来实现的。这些类型的操作之所以可能,是因为分配给这些角色的信任策略。信任策略是附加到 AWS 环境中每个角色的文档。此文…

虚幻5|技能栏优化(1)---优化技能UI,并添加多个技能

一.添加多一个技能格子并进行初始化清楚 1.打开技能UI把原先的事件构造后面的蓝图,全部选中,右键创建一个函数,命名为初始化 2.添加以下两个蓝图,用于清楚技能格子内容 2.在之前,事件构造后面的蓝图,不需…