本篇是「對比 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的高級數據結構,他們底層都遵循了必定的數據結構,但又都有本身的特點。集合本身語言的特性,設計巧妙。總之,無論何種語言,咱們在使用時,既要了解結構的基本用法,還要了解其底層的邏輯結構,才能避免在使用時的一些莫名的坑。