Go常见数据结构的实现原理——map

(一)基础操作

版本:Go SDK 1.20.6

1、初始化

map分别支持字面量初始化和内置函数make()初始化。

字面量初始化:

	m := map[string] int {"apple": 2,"banana": 3,}

使用内置函数make()初始化:

	m := make(map[string]int,10)	// 指定容量可以有效减少内存分配次数,有利于提升程序性能m["apple"] = 2m["banana"] = 3

注意:未初始化的map变量的默认值为nil,向值为nil的map添加元素时会触发panic:assignment to entry in nil map(赋值给空的map),如:

	var m map[string]intm["apple"] = 2					// 触发panic	

在这里插入图片描述

2、增删改查

map的增删改查比较随意…

	m := make(map[string]int,10)m["apple"] = 2			// 添加m["apple"] = 3			// 修改delete(m,"apple")		// 删除v := m["apple"]			// 查询v,exist := m["apple"]	// 查询if exist {fmt.Println(v)}

这里有几个需要注意的地方:

  1. 在上面的修改操作中,如果键"apple"不存在,则会直接执行添加操作。
  2. 删除元素使用内置函数delete()完成,delete()没有返回值,在map为nil或指定的键不存在的情况下,delete()也不会报错,相当于空操作。
  3. 如果使用的是第一种方式查询,当key不存在时,会返回value对应的零值,比如上面会返回0。当使用第二种时,第一个变量为值,第二个为bool类型的变量,用于指示是否存在指定的键,如果键不存在,那么第一个值为同样为对应零值。
  4. map操作不是原子的,当多个协程同时操作map时有可能会产生读写冲突,读写会触发panic

内置函数len()可以查询map的长度,该长度反应map中存储的键值对数。

(二)实现原理

1、数据结构

Go语言的map使用Hash表作为底层实现,一个Hash表里可以有多个bucket,而每个bucket保存了map中的一个或一组键值对。

(1)map的数据结构

map的数据结构由 runtime/map.go:hmap 定义:

type hmap struct {count     int	 	// 当前保存的元素个数flags     uint8		// 状态标志B         uint8  	// bucket 数组的大小noverflow uint16 	// 溢出桶的大概数量hash0     uint32 	// 哈希种子buckets    unsafe.Pointer // bucket 数组,数组的长度为2^Boldbuckets unsafe.Pointer // 老旧bucket数组,用于扩容nevacuate  uintptr        // 表示扩容进度,小于此地址的buckets代表已搬迁完成extra *mapextra // optional fields
}

下图展示了一个hmap.B=2t的map。

在这里插入图片描述

(2)bucket的数据结构

bucket(桶)数据结构由runtime/map.go:bmap定义

type bmap struct {tophash [bucketCnt]uint8	// 长度为8的数组
}
// 底层定义的常量
const (bucketCntBits = 3bucketCnt     = 1 << bucketCntBits		// 一个桶最多有8个位置
)

这是我在书上看到的bucket数据结构,并做出了如下解释:
bucket数据结构中的data和overflow成员并没有显示地在结构体中声明,运行时在访问bucket时直接通过指针的偏移量来访问这些虚拟成员

type bmap struct {tophash [8]uint8	// 存储Hash值的高8位data	[]byte		// key value 数据:key/key/key/.../value/value/value...overflow *bmap		// 溢出bucket的地址
}

每个bucket可以存储8个键值对

  • tophash 是一个长度为8的整型数组,Hash值低位相同的键存入当前bucket时会将Hash值的高位存储在数组中,以方便后续匹配。
  • data 区存放的是key-value数据,存放顺序是 key/key/key/…/value/value/value,如此存放是为了节省字节对齐带来的空间浪费。
  • overflow 指针指向的是下一个bucket,据此将所有冲突的键连接起来。
    在这里插入图片描述

所以tophash到底有什么用?

具体来说,如果两个键的哈希值的低位相同,但高位不同,它们可能会被映射到同一个桶位置。为了区分它们,可以将高位存储在 tophash[i] 数组中。这样,在查找时,可以首先比较低位哈希值,如果相等,再比较高位,以确保正确地匹配到相应的键。

在这种情况下,当添加元素时,如果 tophash[i] 中存储的哈希值与当前 key 的哈希值不相等,可能表示哈希冲突。这时,可能需要通过线性搜索或其他冲突解决方法在当前桶中查找匹配的键。在查找的过程中,可以利用 tophash[i] 数组中的高位信息来进一步确保正确匹配。

总体而言,这种做法是一种提高哈希表性能的优化策略,通过更多的信息来区分相同低位哈希值的键,以减少哈希冲突的影响。在实现哈希表时,具体的优化方法可能会因语言或库的不同而有所不同。

2、哈希冲突

当有两个或以上数量的键被“Hash”到同一个bucket时,我们称这些键发生了冲突。Go使用链地址法来解决冲突。
关于哈希冲突的详细解释可以移步我的这篇博客哈希表是什么
在这里插入图片描述

3、负载因子

负载因子用于衡量一个Hash表冲突情况,公式为:

负载因子 = 键数量/bucket数量

负载因子过小或过大都不理想:

  • 负载因子过小,说明空间利用率低。
  • 负载因子,说明冲突严重,存取效率低

当Hash表的负载因子过大时,需要申请更多的bucket,并对所有的键值对重新组织,使其均匀地分布到这些bucket中,这个过程称为rehash。

4、扩容

(1)扩容条件
为了保证访问效率,降低负载因子,常用的手段是扩容,当新元素将要添加进map时,会判断是否需要扩容。
触发扩容需要满足以下任一条件:

  • 平均负载因子大于6.5
  • overflow的数量达到2^min(15,B)

(2)增量扩容
当负载因子过大时,就新建一个bucket数组,新的bucket数组的长度为原来的2倍,然后旧bucket数组中的数据逐步搬迁到新的bucket数组中。

增量扩容的具体过程是这样的:

1、新建桶数组: 当触发增量扩容时,Go 会创建一个新的、更大的桶数组。

2、元素迁移: 然后,它会逐步将旧桶中的元素重新分配到新的桶数组中,避免一次性大规模的重新哈希。

3、渐进迁移: 在元素逐步迁移的过程中,新添加的元素会直接被放入新的桶数组中,而不会立即迁移。这保证了新元素的添加不会在迁移期间导致性能下降。

4、逐步替换: 最终,当所有元素都成功迁移到新的桶数组后,旧的桶数组会被废弃,新桶数组取而代之,完成了增量扩容的过程。

5、这种增量方式的扩容避免了在添加元素时出现大规模的哈希冲突或性能下降,因为它避免了在一次性扩容中发生的大量元素重新哈希的操作。这种方法相对于整体性地重新哈希整个 map 来说,更加有效和高效。

扩容后示意图:

在这里插入图片描述

搬迁完成后示意图:

在这里插入图片描述

5、增删改查

无论是元素的添加还是查询操作,都需要现根据键的Hash值确定一个bucket,并查询该bucket中是否存在指定的键。

  • 对于查询操作而言,查到指定的键后获取值后就返回,否则返回类型的空值。
  • 对于添加操作而言,查到指定的键意味着当前添加操作实际上是更新操作,否则在bucket中查找一个空余位置并插入。

(1)查找过程

查找过程简述如下:

  1. 计算 Hash 值: 对于给定的 key,通过哈希函数计算其对应的哈希值。
  2. 确定桶位置: 将计算得到的哈希值与当前 map 的桶数量 hmap.B 取模,以确定 key 应该放置在哪个桶中。这个桶就是存储相应 key-value 对的地方。
  3. 查找 TopHash: 从 tophash 数组中获取与当前桶位置对应的 tophash[i],其中 i 是 hash & (hmap.B - 1)。
  4. 比较 Hash 值: 如果 tophash[i] 中存储的哈希值与当前 key 的哈希值相等,那么表示可能找到了对应的桶,需要进一步检查。
  5. 比较实际值: 如果 tophash[i] 中存储的哈希值相等,接下来会比较实际的 key 值。如果找到了匹配的哈希值,但实际 key 不相等,这可能是碰撞,需要继续查找。
  6. 从桶中查找: 如果在当前桶中没有找到匹配的 key,就需要从溢出的桶中继续查找。溢出桶是因为哈希冲突导致多个 key 映射到同一个桶的情况。
  7. 返回结果: 如果找到匹配的 key,就返回对应的 value。如果遍历完所有相关的桶仍然没有找到匹配的 key,则返回相应类型的零值。

如果当前map处于搬迁过程中,则优先从oldbuckets数组中查找,查找到不再从新的buckets数组中查找。

(2)添加过程

新元素的添加过程简书如下:

  1. 根据key值算出Hash值
  2. 取Hash值低位与hmap.B取模来确定bucket位置
  3. 查找该key是否已经存在,如果存在则直接更新值
  4. 如果该key不存在,则从该bucket中寻找空余位置并插入

如果当前map出于搬迁过程中,则新元素会直接添加到新的buckets数组中,但查找过程仍从oldbuckets数组中开始

(3)删除操作

删除元素实际上是先查找元素,如果元素存在则把元素从相应的bucket中清除,如果不存在则什么也不做

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

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

相关文章

Spark SQL 每年的1月1日算当年的第一个自然周, 给出日期,计算是本年的第几周

一、问题 按每年的1月1日算当年的第一个自然周 (遇到跨年也不管&#xff0c;如果1月1日是周三&#xff0c;那么到1月5号&#xff08;周日&#xff09;算是本年的第一个自然周, 如果按周一是一周的第一天) 计算是本年的第几周&#xff0c;那么 spark sql 如何写 ? 二、分析 …

kubernetes集群编排——etcd

备份 从镜像中拷贝etcdctl二进制命令 [rootk8s1 ~]# docker run -it --rm reg.westos.org/k8s/etcd:3.5.6-0 sh 输入ctrlpq快捷键&#xff0c;把容器打入后台 获取容器id [rootk8s1 ~]# docker ps 从容器拷贝命令到本机 docker container cp c7e28b381f07:/usr/local/bin/etcdc…

cadence virtuoso layout drc error

问题&#xff1a; The BORDER layer must enclose all chip layout patterns, which all chip layout patterns include seal ring if seal ring has been added by designers. This rule checking includes the layers of DNW,AA,NW,NC,PC,MVN, MVP,DG,GT,SN,SP,SAB,CT,M1,V1…

C语言——分割单向链表

本文的内容是使用C语言分割单向链表&#xff0c;给出一个链表和一个值&#xff0c;要求链表中小于给定值的节点全都位于大于或等于给定值的节点之前&#xff0c;打印原始链表的所有元素和经此操作之后链表的所有元素。 分析&#xff1a;本题只是单向链表的分割&#xff0c;不涉…

年薪百万的人怎么做好工作复盘和总结

我们在为谁工作&#xff1f; 在大山宏泰《我们为什么工作》一书中有提到过&#xff1a; 70%左右的人认为工作只是维持生计的存在&#xff1b; 20%左右的人认为工作是个人价值的体现&#xff1b; 不到10%的人才会认为工作是幸福的。 人类的终极幸福有四重&#xff1a;被爱&…

Poly风格模型的创建与使用_unity基础开发教程

Poly风格模型的创建与使用 安装Poly相关组件Poly模型的创建Poly模型编辑 安装Poly相关组件 打开资源包管理器Package Manager 在弹出的窗口左上角Packages选择Unity Registry 搜索框搜索 Poly 搜索结果点击Polybrush 点击右下角 Install 同时也别忘了导入一下模型示例&#…

openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-144) - 验证

文章目录 openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-144) - 验证概述笔记重复数字IO的问题想法手工实现程序实现确定要摘掉的数字重合线自动化测试的问题测试程序的场景测试程序的运行效果测试程序实现备注END openpnp - 74路西门子飞达控制板(主控板STM32_NUCLEO-14…

Jenkins的一些其他操作

Jenkins的一些其他操作 1、代码仓库Gogs的搭建与配置 Gogs 是一款极易搭建的自助 Git 服务&#xff0c;它的目标在于打造一个最简单、快速和轻松的方式搭建 Git 服务。使用 Go 语言开发的它能够通过独立的二进制进行分发&#xff0c;支持了 Go 语言支持的所有平台&#xff0…

find和grep命令的简单使用

find和grep命令的简单使用 一、find例子--不同条件查找 二、grep正则表达式的简单说明例子--简单文本查找例子--结合管道进行查找 一、find find 命令在指定的目录下查找对应的文件。 find [path] [expression]● path 是要查找的目录路径&#xff0c;可以是一个目录或文件名…

asp.net core mvc 之 依赖注入

一、视图中使用依赖注入 1、core目录下添加 LogHelperService.cs 类 public class LogHelperService{public void Add(){}public string Read(){return "日志读取";}} 2、Startup.cs 文件中 注入依赖注入 3、Views目录中 _ViewImports.cshtml 添加引用 4、视图使用…

软文推广中媒体矩阵的优势在哪儿

咱们日常生活中是不是经常听到一句俗语&#xff0c;不要把鸡蛋放在同一个篮子里&#xff0c;其实在广告界这句话也同样适用&#xff0c;媒介矩阵是指企业在策划广告活动时&#xff0c;有目的、有计划的利用多种媒体进行广告传播&#xff0c;触达目标用户。今天媒介盒子就来和大…

Hbase 迁移小结:从实践中总结出的最佳迁移策略

在数据存储和处理领域&#xff0c;HBase作为一种分布式、可扩展的NoSQL数据库&#xff0c;被广泛应用于大规模数据的存储和分析。然而&#xff0c;随着业务需求的变化和技术发展的进步&#xff0c;有时候我们需要将现有的HBase数据迁移到其他环境或存储系统。HBase数据迁移是一…

缓存穿透、缓存击穿、缓存雪崩

目录 一、缓存的概念 1.为什么需要把用户的权限放入redis缓存 2.为什么减低了数据库的压力呢&#xff1f; 3.那么什么情况下用redis,什么情况下用mysql呢&#xff1f; 4.关于权限存入redis的逻辑&#xff1f; 二、使用缓存出现的三大情况 1.缓存穿透 1.1概念 1.2出现原…

2023/11/15JAVA学习

如何多开一个程序

企业设备巡检的痛点和解决方案

在设备巡检过程中&#xff0c;企业常面临多种痛点。首先&#xff0c;信息管理不足是一个关键问题&#xff0c;企业往往缺乏全面、准确的设备信息记录&#xff0c;这导致巡检工作缺乏针对性和效率。其次&#xff0c;巡检流程的非标准化使得巡检结果出现不一致&#xff0c;重要的…

RGB转Bayer,一个小数点引发的血案

前几天写了一个RGB数据转Bayer格式的函数&#xff0c;经过测试功能正常。后来把这个函数用到一个数据库构建中&#xff0c;结果数据库出来的结果一直是一张黑图&#xff0c;追查了好几个小时&#xff0c;总算把这只虫子找出来了&#xff0c;原来是一个整数后面的小数点作祟。 …

Docker 和 Kubernetes:技术相同和不同之处

Docker和Kubernetes是当今最流行的容器化技术解决方案。本文将探讨Docker和Kubernetes的技术相似之处和不同之处&#xff0c;以帮助读者更好地理解这两种技术。 Docker和Kubernetes&#xff1a;当今最流行的容器化技术解决方案 在当今的IT领域&#xff0c;Docker和Kubernetes无…

阿里云99元VS腾讯云88元,双11云服务器价格战,谁胜谁负?

在2023年的双十一优惠活动中&#xff0c;阿里云推出了一系列令人惊喜的优惠活动&#xff0c;其中包括99元一年的超值云服务器。本文将带您了解这些优惠活动的具体内容&#xff0c;以及与竞争对手腾讯云的价格对比&#xff0c;助您轻松选择最适合的云服务器。 99元一年服务器优…

23000 个恶意流量代理的 IPStorm 僵尸网络被拆除

美国司法部今天宣布&#xff0c;联邦调查局取缔了名为 IPStorm 的僵尸网络代理服务的网络和基础设施。 IPStorm 使网络犯罪分子能够通过世界各地的 Windows、Linux、Mac 和 Android 设备匿名运行恶意流量。 与此案相关的俄罗斯裔摩尔多瓦籍公民谢尔盖马基宁 (Sergei Makinin)…

传统工艺的数字时代转变:十八数藏的文化创新

在传统工艺和数字时代的交汇之处&#xff0c;十八数藏以其独特的文化创新走在了前列。这场数字时代的转变为传统工艺注入了新的生命和活力。 十八数藏的文化创新并非简单的数字应用&#xff0c;而是一场深刻的转变。通过数字技术&#xff0c;传统工艺被赋予了新的表达方式&…