Map

Go语言的map底层使用Hash表实现的

特性预览

操作方式

初始化

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

字面量初始化
func MapInit() {m := map[string]int{"apple":  1,"banana": 2,}for k, v := range m {fmt.Printf(" %s : %d \n", k, v)}
}
内置函数make()
func MapInit() {m := make(map[string]int, 10)m["apple"] = 1m["banana"] = 2for k, v := range m {fmt.Printf(" %s : %d \n", k, v)}
}

使用make()函数初始化时可以指定map的容量,指定容量可以有效的减少内存的分配次数,有利于提升性能。

增删改查

map的增删改查操作比较简单,如下:

func MapCurd() {m := make(map[string]int, 10)m["apple"] = 1      // addm["apple"] = 2      // updatedelete(m, "apple")  //deletev, ok := m["apple"] //查询if ok {fmt.Println(v)}
}

需要注意的是,在上面的update操作中,如果键"apple"不存在,则map会创建一个新的键值对并存储,等同于add操作。

删除元素使用内置函数delete(),delete()函数没有返回值,在map为nil或者指定键不存在的情况下,delete()也不会报错,相当于空操作。

在查询操作中,最多可以给两个变量赋值,第一个为值,第二个为bool类型的变量,用于指示是否存在指定的键,如果键不存在,则第一个变量为对应类型的零值。如果只指定一个变量,那么该变量仅表示该键对应的值,如果键不存在,同样该值表示的是对应类型的零值。

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

危险操作

并发读写

map操作不是原子的,这意味着多个协程同时操作map时有可能产生读写冲突,读写冲突会触发panic从而导致程序异常退出。

空map

未初始化的map值为nil,在向值为nil的map中添加元素时,会触发panic,在使用时需要额外注意。

值为nil的map,长度与为空的map一样,尽管操作值为nil的map没有意义,但是查询、删除操作不会报错。

小结

  • 初始化map时推荐使用内置函数并指定初始化容量
  • 修改键值对时,需要先查询是否存在,否则会创建新的键值对
  • 查询键值对时,最好检查键值对是否存在,避免零值操作
  • 避免并发读写map,如果需要并发读写,则可以使用额外的锁(互斥锁、读写锁),也可以考虑使用标准库sync包中的sync.Map。

实现原理

数据结构

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

map的数据结构

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

// A header for a Go map.
type hmap struct {// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.// Make sure this stays in sync with the compiler's definition.count     int // # live cells == size of map.  Must be first (used by len() builtin)flags     uint8B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for detailshash0     uint32 // hash seedbuckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growingnevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)extra *mapextra // optional fields
}

下图展示一个有4个bucket的map:

本例中,hmap.B=2,hmap.buckets 数组的长度是4(2的B次方),元素经过Hash运算后会落到某个bucket中进行存储。

bucket很多时候会被翻译成桶,所谓的hash桶实际上就是bucket。

bucket的数据结构

bucket的数据结构是由 src/runtime/map.go:bmap 定义:

// A bucket for a Go map.
type bmap struct {// tophash generally contains the top byte of the hash value// for each key in this bucket. If tophash[0] < minTopHash,// tophash[0] is a bucket evacuation state instead.tophash [bucketCnt]uint8 //存储hash值的高8位// Followed by bucketCnt keys and then bucketCnt elems.// NOTE: packing all the keys together and then all the elems together makes the// code a bit more complicated than alternating key/elem/key/elem/... but it allows// us to eliminate padding which would be needed for, e.g., map[int64]int8.// Followed by an overflow pointer.
}

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

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

注意,bucket的数据结构中的data和overflow成员并没有显式的在结构体中声明,运行时在访问bucket时直接通过指针的偏移来访问这些虚拟成员。

下图展示了bucket存放8个key-value对。

Hash冲突

当有两个或以上数量的键被Hash到同一个bucket时,我们称这些键发生了冲突。Go使用链地址法来解决冲突。由于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会在创建一个键值对,用类似链表的方式将bucket连接起来。

下图展示了产生冲突后的map

在bucket的数据结构中使用指针指向溢出的bucket,表示为当前bucket装不下而溢出的部分。事实上hash冲突并不是好事,因为降低了效率,好的hash算法可以保证hash值的随机性。但无论哪种hash算法,冲突是无法避免的,当冲突比较多的时候就需要选择一些方式来减少冲突。

负载因子

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

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

例如,对于一个bucket数量为4,包含4个键值对的hash表来说,这个hash表的负载因子为1。

负载因子过大或者过小都不好:

  • 过大,说明冲突严重,存取效率低
  • 过小,说明空间利用率低

负载因子过小,可能是预分配的空间太大,也有可能是大部分元素被删除造成的。随着元素不断添加到map中,负载因子会逐渐升高。

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

每个hash表的实现对负载因子的容忍程度不同,比如redis中的map实现中负载因子大于1时就会触发rehash,而Go语言中的map则是在负载因子达到6.5时才会触发rehash,因为redis的每个bucket只能存储一个键值对,而Go的bucket可以存储8个键值对,所以Go的map可以容忍更大的负载因子。

扩容

扩容条件

降低负载因子常用的手段是扩容,为了保证访问效率,当新元素要添加到map时,都会被检测是否需要扩容,扩容实际上是以空间换时间的手段。

触发扩容需要满足以下任一条件:

  • 负载因子大于6.5时,即平均每个bucket存储的键值对达到6.5个以上
  • overflow的数量大于2的15次方,即overflow的数量超过32768

增量扩容

当负载因子过大时,就新建一个bucket数组,新的bucket数组长度是原来的2倍,然后旧bucket数组中的数据搬迁到新的bucket数组中。

考虑到如果map存储了数以亿计的键值对,那么一次性搬迁会造成巨大的延时,所以Go采用了逐步搬迁策略,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。

下图展示了包含1个bucket满载的map(为了描述方便,途中bucket省略了value区域)。

当前map存储了7个键值对,只有一个bucket,根据公式 负载因子=键数量/bucket数量 得出此时负载因子为7。再次添加数据时因为超过了6.5的阈值会触发扩容操作,扩容之后再将新的键值对写入新的bucket中。

当添加第8个键值对时,将触发扩容,扩容后的示意图如下所示:

扩容时的处理非常巧妙,先是让hmap数据结构中的oldbuckets成员指向原buckets数组,然后申请新的buckets数组(长度为原来的两倍),并将数组指针保存到hmap数据结构的buckets成员中。这样子就完成了新老bucket数组的交接,后续的迁移工作将从oldbuckets数组中逐步搬迁到新的buckets数组中,等待oldbuckets搬迁完毕后,就可以安全释放oldbuckets数组了。

搬迁完成后的示意图如下所示:

等量扩容

所谓等量扩容,并不是扩大容量,而是bucket的数量不变,重新再做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存取速度。

在极端的情况下,比如经过大量的元素增删后,键值对刚好集中在一小部分bucket中,这样子会造成溢出的bucket增多,但溢出的bucket中的key-value大部分都是空的,访问效率很差,此时会进行一次等量扩容,即bucket数量不变,经过重新组织后overflow的bucket数量会减少,这样子即减少空间又提高了访问效率。

增删改查

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

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

查找过程

查找过程简述如下:

  • 根据key值计算hash值
  • 取hash值低位为hmap.B取模来确定bucket的位置
  • 取hash值高位,在tophash数组中查询
  • 如果tophash[i]中存储的hash值与当前key的hash值相等,则获取tophash[i]的key值进行比较
  • 当前bucket中没有找到,则依次从溢出的bucket中查找

如果当前map处于搬迁过程中,那么查找时优先从oldbuckets数组中查找,不再从新的buckets中查找。如果查不到,那么也不会返回nil,而是返回相应类型的零值。

添加过程

新元素添加过程简述如下:

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

如果当前map处于搬迁过程中,那么新元素会直接插入到新的buckets数组中,但查找过程仍从oldbuckets数组中开始。

更新操作

更新操作实际上是添加操作的特殊情况,如果元素不存在,则更新操作实际上等同于新增操作

删除操作

删除元素实际上是先查找元素,如果元素存在则把元素从相应的bucket中删除掉,如果不存在则忽略。

golang数据结构初探之字典map相关推荐

  1. golang数据结构初探之管道chan

    golang数据结构初探之管道chan 管道是go在语言层面提供的协程之间的通信方式,比unix的管道更易用也更轻便. 特效速览 初始化 声明和初始化管道的方式主要有以下两种: 变量声明 使用内置函数 ...

  2. golang数据结构初探之字符串string

    字符串string string 是Go语言中的基础数据类型. 特性速览 声明 声明string变量非常简单,常见的方式有以下两种: 声明一个空字符串后再赋值 var s string s = &qu ...

  3. golang数据结构初探之动态数组slice

    动态数组slice slice 又称动态数组,依托于数组实现,可以方便的进行扩容和传递,实际使用时比数组更灵活.但正是因为灵活,实际使用时更容易出错,避免出错的最好方法便是了解其实现原理. 特性速览 ...

  4. golang数据结构初探之iota

    iota Go语言的iota常用于const表达式中,其值是从0开始的,const声明块中每增加一行,iota值都会自增1. 使用iota可以简化常量的定义,但其规则必须牢记,否则在阅读源码时可能会造 ...

  5. Go 字典(Map)

    比较熟悉 Python 的对于字典肯定不会陌生,在 Go 中 map 就是 Python 中字典的概念,它的基本格式是 map[keyType]valueType .map 的读取和设置和 slice ...

  6. golang数据结构_Go数据结构的完整指南

    golang数据结构 This week, in various articles on this blog, I posted an analysis and implementation of t ...

  7. golang实现线程安全的map

    转载自:https://halfrost.com/go_map_chapter_one/ https://halfrost.com/go_map_chapter_two/ Map 是一种很常见的数据结 ...

  8. 鸡尾酒排序算法c语言,[golang] 数据结构-鸡尾酒排序

    吐个槽 又是一个不正经取名的排序算法.真要说和鸡尾酒间的关系,大概就是想喝到鸡尾酒(得到排序好的队列)就要摇晃酒杯让不同的成分混合均匀(向两个方向冒泡排序) 原理 鸡尾酒排序(Cocktail Sor ...

  9. python包含多个元组的元组_Python数据结构(元组,列表,字典)

    Python内置了 几种数据结构,元组,列表 字典 1.元组 元组可以由不同的元素组成,所有元素通过圆括号(  )包含起来,并通过逗号","隔开.如变量名 = (元素1,元素2,. ...

最新文章

  1. 谁在“唱衰”OpenStack?
  2. 安装Ruby、Sass在WebStrom添加Watcher实现编辑scss文件时自动生成.map和压缩后的.css文件...
  3. android---简单的通讯录
  4. 摇一摇 声音 html5,HTML5摇一摇以及音频播放问题优化总结
  5. 从零开始学Pytorch(十七)之样式分格
  6. java cipher用法_java使用Cipher 执行RSA解密报错
  7. IT学子成长指导类文章链接(十二)
  8. js的hasOwnProperty
  9. 多个客户同时连接ServerSocket的简单实现
  10. 深度图像配准_巧解图像处理经典难题之图像配准
  11. 有关access的上机试题_2016年计算机二级《Access》上机操作题及答案
  12. 百度推广怎么调整计算机优先,百度竞价优化关于帐户层级的一些设置方法与技巧...
  13. 金融笔记:货币的概念
  14. Contest1480 - ZZNU17级暑期集训——单人挑战赛(Final)(ZZNU 2145AK王国系列问题-百官排序)
  15. 丧尸的世界·《丧尸西游》
  16. 大数据-孩子学习成绩分析
  17. Gradle原理与瘦包胖包的Jar打包
  18. 最新版db2 express c 10.1安装图解
  19. idea启动不了,报错Warexploded:Artifact is not deployed. Press ‘Deploy’ to start deployment
  20. (寒假集训) Piggyback(最短路)

热门文章

  1. hdu 1907 & hdu 2509(Nim博弈)
  2. bluetooth a2dpsink
  3. 字节辟谣被裁员工与 HR 互殴;苹果头显多个新功能曝光;谷歌希望 RISC-V 成为 T1 级 Android 架构|极客头条...
  4. EXCEL应用:思迈特软件Smartbi数据可视化终极教程
  5. @OneToMany---ManyToOne
  6. 贝叶斯公式——假阳性问题
  7. 采集资源结束后,图片不显示解决办法
  8. 解决:微信小程序+Vant——使用van-grid以及van-card图片加载不出来的问题
  9. 消除WordPress上的渲染阻止JavaScript和CSS
  10. Android菜鸟笔记-WifiPickerActivity 实现跳转到系统自带wifi连接界面