积木成楼
首页 / golang

go design (三) map

2022-01-07 · golang · 约 22 分钟

golang 中 哈希表 map 的实现

哈希表 在 各种语言中有字典,映射 的称呼 ,本质上解决的是 key => value 键值对之间映射关系,因为其读写 O(1) 的复杂度,性能非常优秀,而被广泛使用。

哈希表 的设计原理

如何实现一个优秀的哈希表 ,关键点在于 哈希函数与冲突解决方案。理想的哈希函数 输出范围要大于输入范围,但实际上我们做不到,工程上优秀的 hash 函数,要保证输出均匀分布,复杂度接近 O(1), 而糟糕的 hash 函数 会导致输出不均匀分布,产生大量的冲突则复杂度可能会退化至 O(n)

解决冲突的两种方法

开放寻址法set示意图

拉链法set示意图

golang 中 的 map 的实现

数据结构

  // runtime.map  
  // A header for a Go map. 核心结构
  type hmap struct {
  	// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
  	// Make sure this stays in sync with the compiler's definition.
  	count     int // # 当前哈希表中的元素数量
  	flags     uint8
  	B         uint8  // 表示当前哈希表持有的 buckets 数量,都是2的倍数,所以 len(buckets) == 2^B
  	noverflow uint16 // 溢出桶的大概数目
  	hash0     uint32 // 是哈希的种子,它能为哈希函数的结果引入随机性,这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入
      
  	buckets    unsafe.Pointer // 当前桶的位置 可能为 nil
  	oldbuckets unsafe.Pointer // 哈希在扩容时用于保存之前 buckets 地址的字段,它的大小是当前 buckets 的一半
  	nevacuate  uintptr        // 指示扩容进度,小于此地址的 buckets 迁移完成
  
  	extra *mapextra // optional fields
  }
  
  // mapextra holds fields that are not present on all maps.
  type mapextra struct {
  	// If both key and elem do not contain pointers and are inline, then we mark bucket
  	// type as containing no pointers. This avoids scanning such maps.
  	// However, bmap.overflow is a pointer. In order to keep overflow buckets
  	// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
  	// overflow and oldoverflow are only used if key and elem do not contain pointers.
  	// overflow contains overflow buckets for hmap.buckets.
  	// oldoverflow contains overflow buckets for hmap.oldbuckets.
  	// The indirection allows to store a pointer to the slice in hiter.
  	overflow    *[]*bmap
  	oldoverflow *[]*bmap
  
  	// nextOverflow holds a pointer to a free overflow bucket.
  	nextOverflow *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
  	// 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.
  }
  
  // 编译期间才会动态 生成 真正的结构
  type bmap struct{
      topbits  [8]uint8
      keys     [8]keytype
      values   [8]valuetype
      pad      uintptr
      overflow uintptr
  }

初始化

读、写、删、扩容操作

读,写

删除

扩容

遍历与 map 的无序性

	// decide where to start 生成随机数
	r := uintptr(fastrand())
	if h.B > 31-bucketCntBits {
		r += uintptr(fastrand()) << 31
	}
	// 从哪个 bucket 开始遍历
	it.startBucket = r & bucketMask(h.B)
	// 从 bucket 的哪个 cell 开始遍历
	it.offset = uint8(r >> h.B & (bucketCnt - 1))

关于 map 的几个小问题

map 可以 用 float 做 key 吗?

	m := make(map[float64]int)
	m[2.4] = 2
	
	f1 := 2.4
	f2 := 2.4000000000000000000000001
	fmt.Println(f1 == f2)                       // true
	fmt.Println(m[2.4000000000000000000000001]) // 2

map 可以边遍历 边删除/边写入 吗?

如何比较 两个 map 是否相等

← 返回文章列表