golang特辑 - 简单记录map使用与底层实现

文章目录

  • 什么是map
  • map的使用
  • 底层结构
  • hash冲突时解决方法
    • 两者优缺点

什么是map

    map这个结构在很多编程语言内都有,包括我们今天的主角go语言。今天我们将几个方面为大家分析Go中的Map。

最通俗的话说Map是一种通过key来获取value的一个数据结构,其底层存储方式为数组,在存储时key不能重复,当key重复时,value进行覆盖,我们通过key进行hash运算,然后对数组的长度取余,得到key存储在数组的哪个下标位置,最后将key和value组装为一个结构体,放入数组下标处。

    其中这个有key与value组成的结构体我们称为bucket,会在下文在做介绍。


map的使用

	// 直接声明map并分配内存空间
	var m =  map[string]interface{}{"name": "leslie", "age": 18}
	// 声明map
	var m map[string]string
	// 为map分配内存空间并初始化空值,第二个参数为长度,也可以不传
	m = make(map[string]string, 8)
	// 直接对map赋值
	m["name"] = "leslie"
	m["age"] = "18"
	// 循环遍历map中每一个key&value对
	for key, value := range m {
		fmt.Println(key, value)
	}
	// 对map取值,可以有两个参数返回,第一个返回值为map内对应值,第二个返回值代表是否存在,如果ok为false,则value为默认空值
	value, ok := m["csdn"]
	if ok {
		fmt.Println(value)
	}
	// 删掉map中key为csdn的value
	delete(m, "csdn")
	// len(m)可以获取到当前map的元素长度
	fmt.Println(len(m))

    以上是map比较常用的几个操作,掌握起来还是比较简单的。


底层结构

    刚刚我们提到map底层是一个数组,数组下标是通过key值进行hash运算再去模这个数组长度得到的。而数组内存着是一个我们称为bucket,或者说是bmap的结构体。

    map的源码位于 src/runtime/map.go中:

//bucket结构体定义 b就是bucket
type bmap{
    // 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
    // Followed by bucketCnt keys and then bucketCnt values.
    // NOTE: packing all the keys together and then all the values together makes the    // code a bit more complicated than alternating key/value/key/value/... but it allows    // us to eliminate padding which would be needed for, e.g., map[int64]int8.// Followed by an overflow pointer.
}

    这里英文注释用翻译软件翻译出来有点怪,所以就不翻译了,这里提取几个重要的信息:

tophash为高八位的hash,这样hash值更加简短,每次比较没有必要去拿真实的hash值进行比较。
同时内部的kv存储并不是以k1v1,k2v2,…knvn这样的格式存储。而是k1k2k3…,v1v2v3…这样的格式存储的。
这样的好处是为了防止内存对齐,节省没必要的空间开销 ( ~ ^ ~ 特别巧妙,很细!)

    以下用图简单表示map的结构:

golang特辑 - 简单记录map使用与底层实现_第1张图片
   当往map中存储一个kv对时,通过k获取hash值,hash值的低八位和bucket数组长度取余,定位到在数组中的那个下标,hash值的高八位存储在bucket中的tophash中,用来快速判断key是否存在,key和value的具体值则通过指针运算存储,当一个bucket满时,通过overfolw指针链接到下一个bucket。

   这里观察图片,肯定会有一个疑问不是每个数组元素对应一个kv对吗?为什么bucket里面会有这么多kv?为什么bucket里还会有overfolw这个属性,为什么会不够装呢?别着急,下面介绍。


hash冲突时解决方法

    我们知道hash计算会将一个输入转换成一定长度的字符串,但是难保不齐会出现几个输入经过hash计算后会出现相同字符串的情况,这种情况称为hash碰撞

    而在map中,解决hash冲突一般有两种方式。

  • 线性探测法
        就是按照顺序来,从冲突的下标处开始往后探测,到达数组末尾时,从数组开始处探测,直到找到一个空位置存储这个key,当数组都找不到的情况下回扩容。这个就是用数组的思想。
  • 拉链法
        简单理解为链表,当key的hash冲突时,我们在冲突位置的元素上形成一个链表,通过指针互连接,当查找时,发现key冲突,顺着链表一直往下找,直到链表的尾节点,找不到则返回空。就是我们上图的实现。简单来说,hash冲突后,就和之前的key放在同一个bucket内,溢出的部分通过上文提到overflow相连。

两者优缺点

  1. 由上面可以看出拉链法比线性探测处理简单
  2. 线性探测查找是会被拉链法会更消耗时间
  3. 线性探测会更加容易导致扩容,而拉链不会
  4. 拉链存储了指针,所以空间上会比线性探测占用多一点
  5. 拉链是动态申请存储空间的,所以更适合链长不确定的

你可能感兴趣的:(Go,golang,数据结构,开发语言)