本文已参加「新人创作礼」活动,一起开启掘金创作之路。
今天第一次上直播课,也是我第一次接触 Golang,在听 Go 语法的时候,也是学到了 Go 语言对 map 的使用方法,而作为一个 Acm 现役铁手,不难会对 map 的用法感到熟悉,也不由得对 Go 的 map 感到一丝好奇,所以就浅浅钻研了一下两个 map 之间的区别。
首先,什么是map
In computer science, an associative array, map, symbol table, or dictionary is an abstract data type composed of a collection of (key, value) pairs, such that each possible key appears at most once in the collection.
上面这段话来源于维基百科,译为:“在计算机科学中,关联数组、映射、符号表或字典是由一组(键、值)对组成的抽象数据类型,因此每个可能的键在集合中最多出现一次。”
所以我们不难发现,map作为STL的一个关联容器,它提供一对一(键:值)的数据处理能力,其底层存储方式为数组,在存储时key不能重复,当key重复时,value进行覆盖,也就是说,map就是key-value的数据结构,一个key对应一个value,这个value可以是数字、对象、数组。
如何实现
map的实现主要有两种方式:哈希表(hash table)和搜索树(search tree)。C++中的Map是基于平衡搜索二叉树,即红黑树而实现的,而Go中的map是基于哈希表(散列表)实现的。自此,我们就可以发现它们第一种区别:(有大佬教教我怎么把表格居中嘛,这个markdown有点点不会用...)
| C++中的map | Go中的map | |
|---|---|---|
| 实现方式 | 红黑树(平衡搜索二叉树) | 哈希表(散列表) |
| 查找效率(最优) | O(log n) | O(1) |
| 查找效率(最坏) | O(log n) | O(n) |
| 查找效率(平均) | O(log n) | O(1) |
| 插入、删除效率(最优) | O(log n) | O(1) |
| 插入、删除效率(最坏) | O(log n) | O(n) |
| 插入、删除效率(平均) | O(log n) | O(1) |
| 返回key | 有序 | 无序 |
由于二叉搜索树与哈希表的特性,我们很容易就可以发现以上几种区别,虽然对于哈希表的最坏查找效率会达到O(n),但只要哈希表设计的足够优秀,一般就不会出现最坏的情况。所以,想必大家一定很好奇Go语言的map的底层代码了吧,我们不妨一起来简单看看。
Golang map
通过- The Go Programming Language,我们不难查到Go的源码。因为能力有限,在查阅了相关资料之后,仅仅对map的结构有了初步的认识,大佬们可以直接点击链接开始观看源码,下面也给大家简单列出了原版结构与个人所理解的结构(其实就是带着一点点理解简单翻译了一下嗯)。
// 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 uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets 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 growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
// A header for a Go map.
type hmap struct {
count int // 代表哈希表中的元素个数,调用len(map)时,返回该字段值。
flags uint8 // 状态标志
B uint8 // buckets的对数log 2(哈希表元素数量最大可达到装载因子*2^B)
noverflow uint16 // 溢出桶的大概数量。
hash0 uint32 // 哈希种子。
buckets unsafe.Pointer // 指向buckets数组的指针,数组大小为2^B,如果元素个数为0,它为nil。
oldbuckets unsafe.Pointer // 如果发生扩容,oldbuckets是指向老的buckets数组的指针,老的buckets数组大小是新的buckets的1/2。非扩容状态下,它为nil。
nevacuate uintptr // 表示扩容进度,小于此地址的buckets代表已搬迁完成。
extra *mapextra // 这个字段是为了优化GC扫描而设计的。当key和value均不包含指针,并且都可以inline时使用。extra是指向mapextra类型的指针。
//关于golang的gc(garbage collection)机制,使用方法是标记清理法(三色标记法),另外使用屏障等技术提高回收效率。
}
我们对它有了初步的理解后,在- Go maps in action中,我们也可以发现Go的map的几个特性:
-
使用时必须要make一下,或者使用 :=方式,直接使用未初始化的map则会panic;(注意是使用时,其实这里和C++也产生了一个对比,在C++中只需要重新定义map,即实现了初始化,不需要特意进行类似于make一样的操作;而在Go中,对于不指定初始化大小,和初始化值hint<=8(bucketCnt)时,Go会调用makemap_small函数(- The Go Programming Language),在hint>8时,则调用makemap函数,并直接从堆上进行分配,具体大家可以自己看看源码啦)
-
key的类型必须是可比较类型;
-
不指定迭代顺序,若需要顺序,则需要另外申请slice保存keys,然后sort.Ints(keys);(这一点刚刚说过哦)
简单展示几个小区别
C++
#include <bits/stdc++.h>
using namespace std;
int main()
{
map<string, int> m;
m["a"] = 1;
cout << m.size() << endl; // 1
if (m["b"] == 1 ){} // m["a"] is assigned to 0 automatically and m is changed
cout << m.size() << endl; // 2
return 0;
}
Go
package main
import "fmt"
func main(){
m := map[string]int {"hello": 1}
fmt.Println(len(m)) // 1
if m["world"] == 1 {
}// m["world"] is 0 but m is not changed actually
fmt.Println(len(m), m) // len(m) is still 1
}
结尾
咱就是说,其实还蛮有意思的哈哈哈哈哈,大家能看懂嘛,第一次写笔记,其实这节课除了这些,像是猜数字、字典、Sockets5代理那些都很有意思,收获颇丰呐,期待下节课的学习。
(其实本来还画了几张map实现原理的图的,但是实在有那么亿点点丑,就不拿出来献丑了哈哈哈哈哈哈哈)