【追求卓越09】算法--散列表(哈希表)

引导

        通过前面几个章节的学习(二分查找,跳表),我们发现想要快速查找某一个元素,首先需要将所有元素进行排序,再利用二分法思想进行查找,复杂度是O(logn)。有没有更快的查找方式呢?

        本章介绍一下我们工作中经常接触到的散列表(哈希表)。它能够使查找的效率达到O(1)。主要是理论方面,让大家开始了解哈希思想。

散列表

        提到查找复杂度是O(1),我们在前面接触到的就是数组了。散列思想就是基于数组支持下标随机访问数据特想实现的。那么问题就是如何将元素和数组下标进行映射?

        例:学校开运动会,有100个运动员,编号是0~99。我要查找86号运动员的信息,该怎么做呢?

        思路:我们可以建立一个数组,将标号和数组下标一一对应,访问哪一个标号的运动员,直接查找对应数组下标即可

        该散列思想中,编号我们称作为键或者关键字。将关键字转换为数组下标的,我们称作为散列函数。而散列函数计算得到的值就叫做散列值(数组下标)。

        散列思想:散列表用的就是数组支持按照下标随机访问。当我们通过散列函数将元素的键值映射为数组下标时,然后将数组保存到数组对应位置中。当我们查找都一个元素时,我们使用同一个散列函数,将键值转化为数组下标,从对应的数组下标获取数据。

散列函数

        从散列思想中,我们可以知道散列表中散列函数起到很重要的作用。针对不同的问题,散列函数都可能不同。比如上面的例子,我可以设置这样的哈希函数:

int hash(int key)
{
    return key;
}
/*
将关键字作为下标*/

由于该例子较为简单,也比较容易想到。但是无论什么想的问题,我觉得散列函数应该尽量做到一下几点:

  1. 散列函数计算得到的散列值是一个非负整数。因为散列值会作为数组下标。
  2. 如果key1 == key2,那么hash(key1)==hash(key2)。
  3. 如果key1 != key2,那么hash(key1)!=hash(key2)。

其中1,2好理解,但是3实际上是很实现的。因为数组的大小是有限的,但是不同元素可能会有很多。这就导致肯定会存在key1 != key2,hash(key1)==hash(key2)的情况。这种情况我们称之为散列冲突。再优秀的散列函数也避免不了散列冲突

散列冲突的解决方式

散列冲突的解决方式有两种:开放寻址法,链表法。

开放寻址法

开放寻址法的思想是:出现了散列冲突,我们就重新探测到一个空闲位置,将其插入

如何探测空闲位置?

        如果某个数据经过散列函数之后,存储位置已经被占用了,我们就从当前位子开始,依次往后查找,看是否有空闲位子,直到找到为止。

如何查找?

        通过散列函数求出要查找出的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素。如果相等,则说明是我们要找的元素;否则继续往后查找。如果遍历到空闲位子,还没有找到,则说明散列表中没有要查找的元素。

删除的注意事项?

        我们知道散列表有查找操作肯定也有删除操作。当我们删除一个元素时,如果将其设置为空闲,那么在查找的过程中就会出现问题(原本在散列表中的元素,会认定为不存在)。我们可以将删除的元素置为delete状态。该状态的位子,可以插入数据。查找的时候,不必停下来,继续向后查找。

开放寻址法的缺点?

        从开放寻址法的思想中,我们可以考虑到,当散列表中的元素越来越多的时候,散列冲突可能性越来越大,空闲位置越来越少,线性探测的时间就会越来越久。极端情况下,我们可能需要探测整个散列表,最坏的情况下,时间复杂度是O(n)。

装载因子

一般情况下,我们会尽量保证散列表中有一定比例的空闲槽位,我们用装载因子表示空位的多少。

散列表的装载因子=填入表中的元素个数/散列表的长度

链表法

        链表法相对于开发寻址法较为简单,也容易理解。使用的是指针数组结构。数组元素对应的是一个链表,所有散列值相同的元素都在一个链表中。如图:

        那么链表法的插入操作,我们选择链表的头插,所以时间复杂度是O(1)。

        但是查找和删除操作的时间复杂度是O(k),k表示链表的长度。理论上每个链表的长度应该是均匀的,k=n/m。m是散列表的长度。这就要求散列表的优秀。若散列表不够好,那么查找和删除的复杂度可能会退化到O(n)。

如何设计散列函数

        我们知道散列函数是散列表中重要的一个部分,它的好坏,决定了散列冲突的概率以及散列表的性能。

        散列函数在设计的时候除了尽量遵循,上章节中指明的三点,还要考虑下面几点因素:

  1. 散列函数不能设计的太复杂。复杂的散列函数会消耗过多的资源,间接影响性能。
  2. 散列值尽量随机并且分布均匀。这样才能降低散列冲突的概率,即使出现散列冲突,每个链表长度也比较均匀(针对链表法)

        常见的散列函数的设计方法:数据分析法,直接寻址法,平方取中法,折叠法,随机数法等。

装载因子调整

        我们知道当装载因子过大的时候,说明散列表中的元素越多,空闲位置越少,散列冲突的概率就会越大。

        对于没有频繁插入和删除的静态散列表,我们可以设计出一个完美的散列表。因为数据都是我们提前已知的。

        但是对于动态的散列表,数据集合是动态变化的。我们无法事先申请一个足够大的散列表。但是当装载因子逐渐变大,散列冲突变得无法接受的时候,我们就需要动态扩容。

        比如当散列表的装载因子是0.8时,我们将散列表扩容一倍,那么装载因子就变为了0.4。仅仅将散列表扩容就可以吗?

        散列表扩容之后,我们需要将原先的散列表拷贝到新的散列表中。这个过程需要重新进行散列函数计算。

问题:我们知道在扩容的时候,涉及到数据的搬移,这会消耗很多时间。这就导致响应不及时。该如何处理?

解:这种动态扩容,直接将数据进行搬移的方式,非常的耗时,用户体验很不好(出现概率较低)。我们可以进行优化,将数据搬移的操作进行分批处理。第一次进行扩容,我们只申请散列表,并将数据插入,不进行数据的搬移。当下次再插入数据时,我们从旧的散列表中拷贝一个到新的散列表(旧的delete)。一直当旧的散列表全部被搬移完。在这个过程中,我们查询时,需要先旧的散列表中查询,查询不到,再到新的散列表中查询。如果都没有,说明不存在。这种均摊的方式,将任何情况下,插入操作的时间复杂度是O(1);

选择散列冲突的解决方案

        完全通过动态扩容来解决散列冲突是不可能的。从上一节中,我们知道解决散列冲突的方法有两种,开放地址法和链表法。这两者都项目中都有使用,但是场景不同。

开放地址法

开放地址法,我们知道是将数据都放到数组中,这就能够利用操作系统的局部性原理(CPU缓存),提高访问效率。这是它的优点

缺点:

  1. 删除较为麻烦,需要进行特殊标记
  2. 冲突概率较高
  3. 因为装载因子不能大于1,故相比较于链表法,更加浪费内存空间

总结:开放地址法适用于数据量较小,装载因子较小的情景

链表法

链表法的优点:

  1. 内存的利用率较高,可以使用时再申请
  2. 能够容忍大装载因子。只要元素均分,即使装载因子为10,也只是链表长度变长了。即使链表长度很长,我们也可以通过跳表,红黑树等方式,增加查询效率。

缺点:

  1. 储存在内存中不连续,对CPU不友好
  2. 需要存储指针,浪费内存。(对于大的数据对象,也可以忽略)

总结:链表法适用于数据量大,装载因子较大的场景。适合储存对象比较大

总结

        本章我们主要介绍一些散列表相关的概念:散列函数,散列冲突,散列值,关键值,以及散列冲突的解决方式。

        散列函数设计的好坏决定了散列冲突的概率,也决定了散列表的性能。想要设计一个好的散列表,需要从散列函数,装载因子,散列冲突解决方案着手。

        关于散列函数,我们不能设计的太复杂,并且尽量使散列值均匀分布,这样尽可能减少散列冲突,即便散列冲突,链表长度也较为均匀。

        关于装载因子,我们需要设置一个合理的装载因子上限,并在动态扩容的过程中,需要考虑均分搬移数据

        关于散列冲突解决方案,有开放地址法和链表法。两者都有适用的情景,那么我们就要针对情况进行选择。不过链表法总体而言较为通用

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

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

相关文章

微软发布最新.NET 8长期支持版本,云计算、AI应用支持再强化

11 月 15 日开始的为期三天的 .NET Conf 在线活动的开幕日上,.NET 8作为微软的开源跨平台开发平台正式发布。.NET 团队着重强调云、性能、全栈 Blazor、AI 和 .NET MAUI 是.NET 8的主要亮点。.NET团队在 .NET Conf 2023 [1]活动开幕式上表示:“通过这个版…

nginx 模块相关配置及结构理解

文章目录 模块配置结构模块配置指令先看一下 ngx_command_t 结构一个模块配置的demo简单模块配置的案例演示 模块上下文结构模块的定义 模块配置结构 Nginx中每个模块都会提供一些指令,以便于用户通过配置去控制该模块的行为。 Nginx的配置信息分成了几个作用域(sc…

使用注解的AOP编程

使用注解的AOP编程 当注解没有参数时 当使用注解进行面向切面编程(AOP)时,你可以按照以下步骤来实现: 步骤: 1. 创建自定义注解: 首先,创建自定义的注解,以便在代码中标记需要进…

Excel换不了行怎么解决?

方法一: 使用Alt Enter键 在Excel中,输入文字时按下回车键,光标将会移到下一个单元格,如果想要换行,可以尝试使用Alt Enter键。具体操作如下: 1.在单元格中输入文字; 2.想要换行时,在需要换行的位置按下Alt Enter键; 3…

延时任务定时发布,基于 Redis 与 DB 实现

目录 1、什么是延时任务,分别可以使用哪些技术实现? 1.2 使用 Redis 和 DB 相结合的思路图以及分析 2、实现添加任务、删除任务、拉取任务 3、实现未来数据的定时更新 4、将数据库中的任务数据,同步到 Redis 中 1、什么是延时任务&#xff…

网络运维与网络安全 学习笔记2023.11.23

网络运维与网络安全 学习笔记 第二十四天 今日目标 VRRP负载均衡、BFD原理与配置、BFD典型应用 DHCP工作原理、全局模式DHCP VRRP负载均衡 VRRP单组缺陷 每网段存在一个VRRP组,缺点如下: 主网关数据转发压力大 备份网关不转发任何数据 网络设备利用…

Hook技术(钩子技术)

HOOK(钩子技术) 这里的hook我理解的意思就是通过拦截指令,将指令换成自己想要的指令,从而做道绕过原本的程序指令,要修改这个指令,要用汇编技术,从二进制入手。 扩展: 木马病毒之…

git clone慢的解决办法

在网站 https://www.ipaddress.com/ 分别搜索: github.global.ssl.fastly.net github.com 得到ip: 打开hosts文件 sudo vim /etc/hosts 在hosts文件末尾添加 140.82.114.3 github.com 151.101.1.194 github.global-ssl.fastly.net 151.101.65.194 g…

外部网关协议_边界网关协议BGP

一.边界网关协议BGP的基本概念 边界网关协议(Border Gateway Protocol,BGP)属于外部网关协议EGP这个类别,用于自治系统AS之间的路由选择协议。由于在不同AS内度量路由的“代价”(距离、带宽、费用等)可能不同,因此对于…

elasticsearch 7安装

问题提前报 max virtual memory areas error max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144] 如果您的环境是Linux,注意要做以下操作,否则es可能会启动失败 1 用编辑工具打开文件/etc/sysctl.conf 2 …

qml渲染引擎介绍

qml项目启动入口 Qt Quick项目qml脚本在C++代码里启动,main.cpp如下: #include <QGuiApplication> #include <QQmlApplicationEngine>int main(int argc, char *argv[]) {

VUE excel表格导出

js代码 //下载模板 downloadExl() { // 标题 const tHeader [‘xxx’,xxx,xx名称,电枪xx,协议xx,snxx]; // key const filterVal [agentName, stationName, equName, channelNumber, manufacturer, sn, ]; // 值 const datas [ { agentName: 你好, stationName: 我们, e…

激光雷达与惯导标定 | Lidar_IMU_Init : 编译

激光雷达与惯导标定&#xff1a;Lidar_IMU_Init 编译 功能包安装安装ceres-solver-2.0.0 &#xff08;注意安装2.2.0不行&#xff0c;必须要安装2.0.0&#xff09; LI-Init是一种鲁棒、实时的激光雷达惯性系统初始化方法。该方法可校准激光雷达与IMU之间的时间偏移量和外部参数…

unity shaderGraph实例-可交互瀑布

不要问我水在哪里&#xff0c;你自己相像这是一个瀑布&#xff0c;瀑布的效果我还不会做 效果展示 整体结构 这里片元着色器最后输出的baseColor应该是黑色&#xff0c;白色为错误。 各区域内容 区域1 计算球到瀑布的距离&#xff0c;然后减去一个值&#xff0c;实现黑色区域…

UNETR:用于三维医学图像分割的Transformer

论文链接&#xff1a;https://arxiv.org/abs/2103.10504 代码链接&#xff1a; https://monai.io/research/unetr 机构&#xff1a;Vanderbilt University, NVIDIA 最近琢磨不出来怎么把3d体数据和文本在cnn中融合&#xff0c;因为确实存在在2d里面用的transformer用在3d里面…

wpf使用CefSharp.OffScreen模拟网页登录,并获取身份cookie,C#后台执行js

目录 框架信息&#xff1a;MainWindow.xamlMainWindow.xaml.cs爬取逻辑模拟登录拦截请求Cookie获取 CookieVisitorHandle 框架信息&#xff1a; CefSharp.OffScreen.NETCore 119.1.20 MainWindow.xaml <Window x:Class"Wpf_CHZC_Img_Identy_ApiDataGet.MainWindow&qu…

API自动化测试:如何构建高效的测试流程

一、引言 在当前的软件开发环境中&#xff0c;API&#xff08;Application Programming Interface&#xff09;扮演了极为重要的角色&#xff0c;连接着应用的各个部分。对API进行自动化测试能够提高测试效率&#xff0c;降低错误&#xff0c;确保软件产品的质量。本文将通过实…

SpringMVC(三)

十、拦截器 1、拦截器的配置 SpringMVC中的拦截器用于拦截控制器方法的执行 SpringMVC中的拦截器需要实现HandlerInterceptor SpringMVC的拦截器必须在SpringMVC的配置文件中进行配置&#xff1a; <bean class"com.atguigu.interceptor.FirstInterceptor">…

constexpt

constexpt constexpt是C11引入的新的关键字&#xff0c;它用于在编译时而非运行时计算函数或变量的值。这个特性对于提高程序效率和优化代非常有用。 编译时常量和运行时常量 编译时常量&#xff08;Compile-time Constants&#xff09;和运行时常量&#xff08;Runtime Con…

8年经验之谈 —— 如何使用自动化工具编写测试用例?

以下为作者观点&#xff0c;仅供参考&#xff1a; 在快速变化的软件开发领域&#xff0c;保证应用程序的可靠性和质量至关重要。随着应用程序复杂性和规模的不断增加&#xff0c;仅手动测试无法满足行业需求。 这就是测试自动化发挥作用的地方&#xff0c;它使软件测试人员能…