本篇是「对比 Python 学习 Go」系列的第四篇,本篇文章咱们来看下 Go 的高级数据结构,因文章偏长分为两篇,此为下篇。本系列的其余文章可到 「对比 Python 学习 Go」- 开篇 查看,下面咱们开始今天的分享。html
上篇说道,Go和Python的数据结构可分为类数组和哈希结构。本篇咱们来看下哈希结构相关的类型。python
哈希结构又叫作散列表(hash table),它是数组的一种扩展。它经过散列函数把元素的键值映射为数组的下标,而后将数据存储在数组中对应下标的位置。当咱们按照键值查询元素时,咱们用一样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。经过散列函数,咱们能够相似数组下标同样直接定位数据,时间复杂度能够到O(1)。git
哈希结构中,最重要的两个知识点是「哈希函数的构建」和「散列冲突的解决」。github
哈希函数构建的好坏直接影响到数据结构的性能,哈希的key 分布均匀的话,会减小散列冲突的发生。 散列冲突是哈希结构不可避免的,解决散列冲突的方法主要有两种,是「开放寻址法(open addressing)」和「列表法(chaining)」。golang
开发寻址法,即利用一些算法查找下一个为空的数组位置。列表法,是在当前key的数组位置,以链表的形式,增长额外空间。web
更多哈希知识,可参考我整理的有关散列表的笔记 数据结构与算法 - 散列表。算法
了解了上边列出的哈希结构的基本知识后,咱们来看看Go和Python的哈希结构是如何的。c#
Go语言的中的哈希结构为 map 结构,根据map的源码分析,map的底层结构大体以下:数组
最外层为一个hmap的结构体,使用一个[]bmap数组存放了bmap的地址,bmap用来存储数据,每一个bmap最多可存储8个kv对,另外还有一个overflow,存储后一个bmap的地址。 oldbuckets 用来存放老的buckets数组地址,在扩容的时候会使用来暂存尚未移到新buckets数组的bmap地址。mapextra 用来存放非指针数据,用于优化存储和访问。缓存
关于map内存的增加扩容,则主要是[]bmap数组的扩容。当数据愈来愈多时,[]bmap数组后边挂的bmap也会愈来愈多,bmap的数量越多,在查找时,则大部分时间会花费在链表的查找上。这里有个标准,一般是在装填因子(填入表中的元素个数/散列表的长度)大于6.5时,会触发[]bmap数组的扩容,一般是源数组的两倍。扩容后,并不会当即迁移数据,而是先将老的[]bmap数组挂在olebuckets上,待有新的更新或插入操做时,才进行bmap的迁移。
根据咱们对Go map内存结构的分析,结合散列表的知识,咱们能够知道,Go使用了「链表法」来解决散列冲突。只不过,链表中的节点并不是是值,而是一个bmap结构的存储块,这样能够减小单个链上的对象块,方便内存管理,利于GC操做。
在哈希函数方面,采用哈希低位肯定bmap,高位对比肯定是否有存储的key,提升了哈希比对搜索的效率。
另外一个在bmap中,并无key-value结对存储,而是将相对占用空间小的key放一块,value按相同的顺序放一块。这样利用内存对齐,节省空间。
Go map的设计到处透露着对性能的极致追求,强烈建议好好研究一番。
下面咱们来看看Go map的一些经常使用操做:
// 初始化
// 使用make函数
myMap := make(map[string]int)
fmt.Println(myMap) // map[]
// 使用字面量
myResume := map[string]string{"name": "DeanWu", "job": "SRE"}
fmt.Println(myResume) // map[job:SRE name:DeanWu]
// 声明一个空map
//var myResume1 map[string]string
//myResume1["name"] = "DeanWu" //panic: assignment to entry in nil map
// 空的map,系统并无分配内存,并能赋值。
// 键值的类型能够是内置的类型,也能够是结构类型,只要这个值可使用 == 运算符作比较
// 切片、函数以及包含切片的结构类型,这些类型因为具备引用语义,可被其余引用改变原值,不能做为映射的键。
//myMap1 := map[[]string]int{}
//fmt.Println(myMap1) // invalid map key type []string
// 更新、赋值key
myResume["job"] = "web deployment"
fmt.Println(myResume) // map[job:web deployment name:DeanWu]
// 获取某个key的值
value, exists := myResume["name"]
if exists {
fmt.Println(value) // DeanWu
}
value1 := myResume["name"]
if value1 != ""{
fmt.Println(value1) // DeanWu
// 推荐上边的写法,由于即便map无此key也会返回对应的零值。须要根据数据类型,作相应的判断,不如上边的统一,方便。
}
// 删除键值对
delete(myResume, "job")
delete(myResume, "year") // 当map中没有这个key时,什么都不执行。
fmt.Println(myResume) // map[name:DeanWu]
复制代码
map 也能够嵌套。
// map嵌套
myNewResume := map[string]map[string]string{
"name": {
"first": "Dean",
"last":"Wu",
},
}
fmt.Println(myNewResume) // map[name:map[first:Dean last:Wu]]
复制代码
Python 中的哈希结构,有字典和集合两种。
字典
字典根据Python3的源码,底层结构大体以下:
其中最外层是PyDictObject,其中定义了一些字典的全局控制字段。其中有个PyDictKeysObject定义了字典哈希表的一些字段。其中有两个数组 dk_indices[]
和 dk_entries[]
,这两个即是真正的存储数据的数组。kv数据保存在dk_entries[]
数组中,dk_indices[]
来存储kv数据在dk_enties
数组中保存的索引。其中每一个kv数据以entry
的数据结构来存储,以下:
typedef struct {
/* Cached hash code of me_key. */
Py_hash_t me_hash;
PyObject *me_key;
PyObject *me_value; /* This field is only meaningful for combined tables */
} PyDictKeyEntry;
复制代码
me_hash
缓存存key的哈希值,防止哈希值的重复计算。me_key
和me_value
即是key和value的真正数据了。
哈希表的扩容,从源码中能够看出,一个字典的最小容量为8,Python 采用了"翻倍扩容"的策略。根据经验值得出,哈希表数组中,装填因子为2/3时,是一个哈希冲突的临界值。因此,当哈希数组dk_entries
装填因子到2/3时,便会扩容。
这里Python为了节省内存,将索引和哈希表数组分开,分为dk_indices
和dk_entries
。前者保存的是数据的索引,占空间小,可申请全部元素个数的空间。后者能够只申请原大小的2/3空间。由于到2/3以后,便会扩容,这个2/3能够根据dk_indices
得到。
分析了Python 字典的底层结构,根据哈希表的知识,咱们能够知道Python 是用「开放寻址法」来解决哈希冲突的。
Python 字典的经常使用操做:
# 初始化
myDict1 = dict()
myDict2 = {} # 推荐使用
print(myDict1, myDict2) # {} {}
# 赋值
myDict3 = {'name': 'Tim', 'age': 18}
myDict3['job'] = 'student'
print(myDict3) # {'name': 'Tim', 'age': 18, 'job': 'student'}
# 取值
print(myDict3['name']) # Tim
# print(myDict3['phone']) # KeyError: 'phone'
print(myDict3.get('phone')) # None 若没有key,使用get 方法不会抛出错误
print(myDict3.get('phone', '136xxxxxxx')) # 136xxxxxxx 给没有key的,附默认值
# 删除
del[myDict3['job']]
print(myDict3) # {'name': 'Tim', 'age': 18}
# 字典提供丰富的内建方法
# radiansdict.clear() 删除字典内全部元素
# radiansdict.copy() 返回一个字典的浅复制,返回原字典的引用
# radiansdict.fromkeys() 建立一个新字典,以序列seq中元素作字典的键,val为字典全部键对应的初始值
# radiansdict.get(key, default=None) 返回指定键的值,若是值不在字典中返回default值
# key in dict 若是键在字典dict里返回true,不然返回false
# radiansdict.items() 以列表返回可遍历的(键, 值) 元组数组
# radiansdict.keys() 以列表返回一个字典全部的键
# radiansdict.setdefault(key, default=None) 和get()相似, 但若是键不存在于字典中,将会添加键并将值设为default
# radiansdict.update(dict2) 把字典dict2的键/值对更新到dict里
# radiansdict.values() 以列表返回字典中的全部值
# pop(key[,default]) 删除字典给定键 key 所对应的值,返回值为被删除的值。key值必须给出。 不然,返回default值。
# popitem() 随机返回并删除字典中的一对键和值(通常删除末尾对)。
复制代码
集合
集合和字典同样,底层也是哈希结构,和字典相比,可理解为只有key,没有values。根据Python3源码,大体结构以下:
相比字典,集合简单了很多。在PySetObject
中直接保存了存储数据的数组。
根据集合的底层数据结构分析,它解决哈希冲突也是使用的「开发寻址法」。
集合的一些经常使用操做:
# 初始化
s1 = {'1', '2', '3'} # 不推荐,当元素中有字典时,会报错
s2 = set(['1', '4', '5'])
print(s1) # {'3', '1', '2'}
print(s2) # {'3', '1', '2'}
# 交集
print(s1&s2) # {'1'}
# 并集
print(s1|s2) # {'3', '5', '4', '2', '1'}
# 差集
print(s1 - s2) # {'3', '2'}
# 判断子集和超集
s2.issubset(s1) # s2 是否为s1 的子集
s1.issuperset(s2) # s1 是否为 s2 的超集
# 集合的一些内建方法
# set.add(obj) 添加集合元素
# set.remove(obj) 删除集合元素
# set.update(set) 合并集合
# set.pop() 随机删除一个元素,并返回该元素
复制代码
除了类数组和哈希结构外,Go还有本身独有的一些结构。
Go 语言具备指针。 指针保存了变量的内存地址。类型 *T是指向类型 T的值的指针。其零值是 nil。
与 C 不一样,Go 没有指针运算。
i, j := 42, 2701
p := &i // point to i
fmt.Println(*p) // read i through the pointer
*p = 21 // set i through the pointer
fmt.Println(i) // see the new value of i
p = &j // point to j
*p = *p / 37 // divide j through the pointer
fmt.Println(j) // see the new value of j
复制代码
Python 中并无指针的概念,内存的地址被叫作“引用”。和这里的指针有殊途同归之妙,但仅仅是体如今逻辑分析上,并无语法支持。
Go语言中,结构体(struct)就是一个字段的集合,结构体字段能够经过结构体指针来访问。
// 定义结构体,自动名必须大写开头,做为公共变量
type Vertex struct {
X int
Y int
}
// 初始化
var ver Vertex
ver.X = 4 // 可以使用. 来赋值和访问结构体
fmt.Println(ver.X) // 4
// 可以使用指针来访问
v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v) // {1000000000 2}
复制代码
结构体能够实现嵌套,当嵌套时,会继承嵌套结构体的全部字段。
type NewVertex struct {
Vertex
Z int
}
var v1 NewVertex
v1.X = 12
v1.Z = 13
fmt.Println(v1.X) // 12
fmt.Println(v1) // {{12 0} 13}
复制代码
正由于结构体的上边的这些特性,加之Go语言中并无类的概念,结构体在不少Web框架中,被当作“类”来使用。
本篇咱们我学习了Go和Python的高级数据结构,他们底层都遵循了必定的数据结构,但又都有本身的特点。集合本身语言的特性,设计巧妙。总之,无论何种语言,咱们在使用时,既要了解结构的基本用法,还要了解其底层的逻辑结构,才能避免在使用时的一些莫名的坑。