• 再也不怕面试官拷打Go数据结构了!-Go语言map详解


    map是Go语言中的哈希表,用来存储key/value键值对。可以在O(1)的时间复杂度内进行查找。map是Go语言在面试中经常会被问到的一个点,这篇文章就来细细梳理一下map的相关知识点,面试不再怕,offer顶呱呱。

    哈希冲突

    我们知道,哈希表是存储键值对的常用数据结构,而哈希表经常会遇到哈希冲突这一情况,指的是两个key/value键值对计算得出的hash值相同,会存储到同一个位置,就会发生撞车。解决方式有两种:拉链法或者开放寻址法。

    • 拉链法:将数组中的元素换成指针,数组中的每个元素指向一个链表。遇到哈希冲突的情况就将冲突的元素连接到链表后面。拉点发处理冲突简单,但是当链表长度过长时,可以采用优化策略,例如采用红黑树代替链表。
    • 开放地址法:简单来说就是当某个位置冲突时,就继续往后寻找空位,直到找到未使用的数据槽,将数据放入。

    在Go语言中如何处理哈希冲突?小孩子才做选择,成年人当然全都要啦。

    Go语言中map的底层结构

    在Go语言中,map的底层结构是一个指向hmap的指针,占用8个字节。hmap中包含多个元素结构为bmap的bucket数组。bucket的底层采用链表将这些bmap连接起来。这里处理冲突用到了优化的拉链法,链表中的每个节点存储的不是一个键值对,而是8个键值对。
    map的数据结构在源码结构中的关键字段如下,在src/runtime/map.go中

     type hmap struct {
         count     int     // 元素的个数
         flags     uint8   // 状态标志位,标记map的一些状态
         B         uint8   // buckets 数组的长度就是 2^B 个
         noverflow  uint16  // 溢出桶的数量 
         
         buckets    unsafe.Pointer  // 2^B个桶对应的数组指针,buckets数组的元素是bmap
         oldbuckets unsafe.Pointer  // 发生扩容时,记录扩容前的buckets数组指针
         
         extra *mapextra // 用于保存溢出桶的地址
     }
     
     type mapextra struct {
         overflow    *[]*bmap  // 溢出桶链表地址
         oldoverflow *[]*bmap  // 老的溢出桶链表地址
     
         nextOverflow *bmap  // 下一个空闲溢出桶地址
     }
     
     type bmap struct {
         tophash   [8]uint8  // 存储了bmap中8个k/v键值对的每个key根据哈希函数计算得出的hash值的高8位
         keys      [8]keytype // 存储了bmap中8个k/v键值对中的key
         values    [8]valuetype  // 存储了bmap中8个k/v键值对中的key
         overflow  uintptr // 指向溢出桶的指针
     }
     
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在这里插入图片描述

    map的访问原理

    对map的访问两种方式:

    v := map[key]     // 当map中没有对应的key时,会返回value对应类型的零值
    v, ok := map[key] //当map中没有对应的key时,除了会返回value对应类型的零值,还会返回一个值存不存在的布尔值
    
    • 1
    • 2

    map的访问步骤:

    1. 判断map是否为空或者无数据,若为空或者无数据返回对应的空值
    2. map写检测,若正处于写状态,表示此时不能进行操作,报fatal error
    3. 计算hash值和掩码
    4. 通过最后的B位确定在哪号桶。
    5. 根据key对应的前8位快速确定是在这个桶的哪个位置
    6. 对比key完成的hash是否匹配,匹配则获取对应value
    7. 如果都没有找到,就去连接的下一个溢出桶去找。

    这里也可以看出,tophash的作用是快速确定key是否正确,也可以理解成一种缓存措施,如果前8位都不对,那后面也没必要比较了。
    在这里插入图片描述

    map的赋值原理

    map的赋值操作很简单:

    map[key] = value
    
    • 1

    原map中存在key时,则更新对应的值为value,若map中不存在key时,则插入键值对key/value。

    注意

    • 对map赋值时,map一定先进行初始化。map初始化有:
    m := map[key]value{}
    m := make(map[key]value)
    
    • 1
    • 2
    • map是非线程安全的,不支持并发读写操作。当其他线程正在读写map时,执行map的赋值会报并发读写错误。
      **

    map的赋值步骤:

    **
    map写检测,如果当前正处于写状态,表示此时不能进行赋值。
    计算hash值,将map置为写状态
    通过key的hash值后B位确定是哪一个桶
    遍历当前桶,通过key的tophash和hash值,防止key重复,然后找到第一个可以插入的位置,即空位置存储数据。
    如果当前桶元素已满,则会通过overflow连接创建一个新的桶。
    再次判断map的写状态
    清除map的写状态

    map的扩容

    map在两种情况下会触发扩容:

    • map的负载因子(当前map中,每个桶中的平均元素个数)已经超过6.5

    正常情况下,如果没有溢出桶,每一个桶中最多有8个元素,当平均每个桶中的数据超过6.5个,那就意味着当前容量不足了,要扩容。

    • 溢出桶的数量过多

    当B < 15时,如果overflow的bucket数量超过2^B
    当B >= 15时,overflow的bucket数量超过2^15
    简单来讲,新加入的key的hash后B位都一样,使得个别桶一直在插入新数据,进而导致它的溢出桶链条越来越长,如此一来,map在操作数据时就会变得很慢。及时扩容,可以重排数据,使元素在桶中的位置更加平均。
    对应的,有两种不同的扩容策略:

    • 双倍扩容(负载因子 > 6.5)
    • 等量扩容(溢出桶过多)

    双倍扩容

    发生这种扩容是由于当前桶数组实在不够用了,这种扩容元素会重排,可能会发生桶迁移。

    如图,扩容前B=2,扩容后B=3,假设一元素key的hash值后三位为101,那么由前文可知,扩容前,由hash的后两位决定几号桶,即01所以元素在1号桶。在扩容发生后,由hash的后三位来决定在几号桶,即101,所以元素会迁移到5号桶。

    在这里插入图片描述

    等量扩容

    由于map中不断put和delete key,桶中可能会出现很多断断续续的空位,这些空位导致连接的bmap很长,导致扫描时间变长。这种扩容实际上是一种整理,把后置位的数据整理到前面,这种情况下,元素会重排,但不会换桶。
    在这里插入图片描述

  • 相关阅读:
    Django 实现连续请求
    人血白蛋白修饰银纳米簇(HSA-AgNCs),间苯二酚/丹酚酸A偶联人血清白蛋白
    【Latex】模板设置及使用教程
    【视觉SLAM14讲】【汇总】
    python基于PHP+MySQL的在线考试系统
    信息学奥赛一本通 1353:表达式括号匹配(stack) | 洛谷 P1739 表达式括号匹配
    微软开源 windows-drivers-rs,用 Rust 开发 Windows 驱动程序
    [Shell详解-7]:循环语句
    设计模式day13
    (自学)黑客技术——网络安全
  • 原文地址:https://blog.csdn.net/m0_73728511/article/details/133150071