「對比Python學習Go」- 高級數據結構上篇

本篇是「對比 Python 學習 Go」 系列的第四篇,本篇文章咱們來看下 Go 的高級數據結構,因文章偏長分爲兩篇,此爲上篇。本系列的其餘文章可到 「對比 Python 學習 Go」- 開篇 查看,下面咱們開始今天的分享。html

Python 數據結構底層徹底依賴解釋器的實現方式,沒有特殊說明文中數據結構對應默認解釋器 CPython。python

從數據結構上來說,有「數組」和「鏈表」兩種基本的數據結構,還有不少基於他們的高級數據結構如棧、隊列、散列表等等。做爲編程語言,Go 和 Python 是如何定義本身的數據結構的呢?根據數據結構的特性,咱們大體能夠將 Go 和 Python 的數據結構分以下幾個類型:git

  • 「類數組的結構」,具備數組的一些特性,但不徹底是數組。
  • 「哈希類型」,即 key-value 類型或叫 map 類型。
  • 語言本身特有的一些高級結構。

下邊咱們來逐一介紹。github

類數組結構

數組,它是一個線性表的結構。它有以下特性:golang

  • 使用一組連續的內存空間來存儲數據。
  • 存儲的數據具備相同類型

回顧了數組的特性,咱們來看下 Go 和 Python 中有哪些類數組的數據結構。編程

Go

在 Go 語言中,有「數組」和「切片」兩個類數組數據結構。c#

數組數組

Go 的數組特性可總結以下:緩存

  • 固定長度:這意味着數組不可增加、不可縮減。想要擴展數組,只能建立新數組,將原數組的元素複製到新數組。
  • 內存連續:這意味着能夠經過下標的方式(arr[index])索引數組中的元素。
  • 固定類型:固定類型意味着限制了每一個數組元素能夠存放什麼樣的數據,以及每一個元素能夠存放多少字節的數據。

數組的初始化和操做以下:markdown

package main

import "fmt"

func main() {
    // 類型 [n]T是一個有 n個類型爲 T的值的數組
    // 先聲明,後賦值
    var a [2]string
    a[0] = "Hello"
    a[1] = "World"

    // 聲明時,直接賦值
    b := [5]int{10,20,30,40,50}

    // 可直接經過下標來訪問數組
    fmt.Println(a)
    fmt.Println(a[0], a[1])
    fmt.Println(b)
    fmt.Println(b[1], b[2])

    // 經過len()函數可獲取數組長度
    fmt.Println(len(a))
    fmt.Println(len(b))

    // 數組元素賦值
    b[1] = 25
    fmt.Println(b)
    fmt.Println(b[1])

}

複製代碼

除了普通數組以外,還有多維數組,即數組的數組。

// 聲明一個二維整型數組,兩個維度分別存儲 4 個元素和 2 個元素
var arr [4][2]int
arr[0] = [2]int{10, 11}
arr[0][1] = 15

// 聲明時,直接賦值
arr1 := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}

// 聲明時,使用下標賦值
arr2 := [4][2]int{1: {20, 21}, 3: {40, 41}}
arr3 := [4][2]int{1: {0: 20}, 3: {1: 41}}

// 使用數組類初始化數組
var arr4 [2]int = arr1[1]
// 使用數組元素來賦值普通元素
var value int = arr1[1][0]

// 使用索引獲取數組值
fmt.Println(arr)
fmt.Println(arr1)
fmt.Println(arr1[0][1])
fmt.Println(arr2)
fmt.Println(arr3)
fmt.Println(arr4)
fmt.Println(value)

// [[10 15] [0 0] [0 0] [0 0]]
// [10 15]
// [[10 11] [20 21] [30 31] [40 41]]
// 11
// [[0 0] [20 21] [0 0] [40 41]]
// [[0 0] [20 0] [0 0] [0 41]]
// [20 21]
// 20

複製代碼

數組中未被初始化的元素,會自動初始化爲各種型對應的「零值」。

切片

Go 的數組長度是固定的,內存空間中存儲了數據值,當存儲的量特別大的時候,使用起來極不方便。在 Go 語言中,除了數組還定義了一個類數組結構,叫作「切片(slice)」。

切片是數組段的描述符。它由一個指向底層數組的指針,數組段長度及其容量(段的最大長度)組成。切片更像是數組的引用類型,而數組則是值類型。其結構以下:

slice

在 Go 的編程中,因爲數組的各類侷限性,對數據集合類型的操做處理時,切片是首選的數據結構。切片的底層數據存儲仍是數組,因此數組的一些特性切片也有。

// 切片使用make(slice, len, cap) 聲明, cap可省略,省略時,等於len
sli := make([]int, 2, 3)
fmt.Println(sli)  // 未賦值時,各元素爲對應的零值
fmt.Printf("%p %v %v \n", sli, len(sli), cap(sli))

// 聲明時,初始化
nums := []int{10, 20, 30, 40}
fmt.Println(nums)
fmt.Printf("%p %v %v \n", nums, len(nums), cap(nums))

// nil 切片,指針爲空,長度和容量爲0
var nums1 []int
fmt.Println(nums1)

// 空切片,指針爲指向一個空數組,長度和容量爲0
var nums2 = make([]int, 0)
nums3 := []int{}
fmt.Println(nums2)
fmt.Println(nums3)

nums[0] = 12
fmt.Println(nums)

// 從切片建立切片
fmt.Println(nums)
fmt.Println(nums[1:2]) // 長度爲2-1 =1,容量1
fmt.Println(nums[1:2:4]) // 長度爲2-1=1,容量爲4-1=3
//slice[i:] // 從 i 切到最尾部
//slice[:j] // 從最開頭切到 j(不包含 j)
//slice[:] // 從頭切到尾,等價於複製整個 slice

// 切片的追加, 使用內建函數 append(src, item) 返回 新的切片
nums = append(nums, 10) // 添加一個
nums = append(nums, 10, 20) // 同時添加多個
newNums := append(nums, nums1...)  // 合併兩個切片
fmt.Println(newNums)
fmt.Println(nums)

// 切片的複製, 使用內建函數 copy(dst, src) 返回複製的元素個數
// 賦值時,接收的切片容量須要大於原切片,不然複製失敗,且不會報錯
copyNums := make([]int, 5)
count := copy(copyNums, nums)
fmt.Println(count)
fmt.Println(copyNums)
fmt.Println(nums)
複製代碼

上面說到,Go 的切片歸根接地是一個數據段的描述符。底層是引用的數組結構,當多個切片同時引用一個數組時,使用下標來修改切片,則會相互的影響,使用時,必定要注意。

// 數組共享的切片
sli := make([]int, 3)  // 定義一個長度,大小都爲3的切片
sli1 := sli[:2]  // 由切片再建立切片,sli 和sli1 底層引用同一個數組
// slice: 0xc0000160c0 ,len: 3 ,cap: 3
fmt.Printf("slice: %p ,len: %v ,cap: %v \n", sli, len(sli), cap(sli))
// slice1: 0xc0000160c0 ,len: 2 ,cap: 3
fmt.Printf("slice1: %p ,len: %v ,cap: %v \n", sli1, len(sli1), cap(sli1))


sli[0] = 10
fmt.Println(sli)   // [10 0 0]
fmt.Println(sli1)  // [10 0]

複製代碼

切片的容量,可在使用中動態增加。切片的動態增加是經過內置函數 append() 來實現的,它會自動的處理好切片擴縮容的全部細節。擴容長度一般是原來切片容量的一倍,當容量大於 1024 時,增加變爲原來容量的 1/4 倍。

// 切片擴容
fmt.Printf("%p %v %v \n", sli, len(sli), cap(sli)) // 0xc0000160c0 3 3
sli = append(sli, 1)
sli = append(sli, 2)
fmt.Println(sli)  // [10 0 0 1 2]
fmt.Printf("%p %v %v \n", sli, len(sli), cap(sli)) // 0xc00001a0c0 5 6

複製代碼

上邊代碼中 sli 長度爲 3,容量爲 3。append 元素以後,由於容量已經被佔滿,因此自動擴容了一倍的容量,通過兩次 append 以後,長度變爲 5,容量爲 6。除了長度和容量外,你們可能發現了,數組地址變了,這說明 append 函數在擴容的時候,會建立一個新的底層數組,而並不是在原數組上進行直接追加擴容。

append 函數擴容建立新數組,這時候再經過下標來修改數據或繼續執行 append 是不會覆蓋原底層數組的,由於已經不是一個數組了。這個特色常常被用在從一個切片建立另外一個切片時,防止切片賦值相互影響。

sli := make([]int, 3)  // 定義一個長度,大小都爲3的切片
fmt.Println(sli)  // [0 0 0]
sli1 := sli[:3:3]  // sli1 長度3-0=3,容量爲3-0=3
fmt.Println(sli1)  // [0 0 0]
fmt.Printf("%p %v %v \n", sli, len(sli), cap(sli))  // 0xc0000160c0 3 3
fmt.Printf("%p %v %v \n", sli1, len(sli1), cap(sli1))  // 0xc0000160c0 3 3
sli1 = append(sli1, 2)  // 容量已滿,新建立底層數組
fmt.Printf("%p %v %v \n", sli1, len(sli1), cap(sli1))  // 0xc00001a0c0 4 6

複製代碼

Python

Python 中的類數組數據結構,爲列表(List)和元組(tuple)。

List

列表,與數組相比,更爲高級,列表的底層結構以下:

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;
複製代碼

拋去公共的頭部變量PyObject_VAR_HEAD,列表由一個指針數組ob_item,和一個容量allocated組成。結構以下:

list

列表中,並未存儲實際的數值,而是存儲了數值的引用地址,引用地址能夠指向任意類型的數據,也就能夠理解爲何列表中能夠有任意類型的元素了。另外一個,列表中引用地址的大小相同,保存在一個連續的存儲空間,也就有了數組的一些特性,能夠經過下標快速定位。

根據列表底層結構和 Python 官方文檔 How are lists implemented in CPython? 總結 List 有以下特性:

  • 列表元素可使用下標索引取值,各元素是有位置順序的,底層爲連續的存儲空間。
  • 列表中存儲的是數據的內存地址,並不是真實數據。因此從上層結構看,list 列表能夠存儲任意類型,即列表元素中的內存地址能夠是指向任意類型的。
  • 能夠任意添加新元素,要能不斷地添加新元素,其使用了「動態擴充」的策略。擴容策略的增加倍數大體是這樣的:0, 4, 8, 16, 24, 32, 40, 52, 64, 76...,參考源碼 listobject.c

列表的初始化及操做以下:

# 列表的初始化
l1 = []  # 推薦,更快速
l2 = list()
l3 = [1,2,3,4]

# 列表相加
print(l1+l2)
print(l1*2)  # 可以使用*來複制列表

l3 += l1
print(l3)

# 列表取值
print(l1[2])
print(l3[1:2])  # 從下標1到下標2
print(l3[1:])  # 到結尾
print(l3[:2])  # 從0開始

# 列表的長度
print(len(l1))

# 刪除索引爲3的元素
del l1[3]

複製代碼

Python 的列表做爲內建的高級數據結構,實現了一系列的操做功能函數。

dir(list)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__',
 '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__',
 '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__',
 '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__',
 '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__',
 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

l1 = [1, 2, 3, 4, 5]
l2 = [6, 7, 8, 9, 10]

# 列表追加
l3 = l1.append(6)

# 列表合併
l4 = l1.extend(l2)

# 列表元素的索引
print(l1.index(2))  # 2在l1列表中的索引

# 插入列表元素
print(l1)  # [1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10]
l1.insert(2, 12)  # 在索引爲2的位置插入值12,原值及後邊的值後移。
print(l1)  # [1, 2, 12, 3, 4, 5, 6, 6, 7, 8, 9, 10]

# 彈出列表指定索引的元素
num = l1.pop()  # 默認爲最大索引
print(num)  # 10
print(l1)  # [1, 2, 12, 3, 4, 5, 6, 6, 7, 8, 9]
num = l1.pop(3)  # 彈出索引爲3的元素,後邊的元素前移
print(l1)  # [1, 2, 12, 4, 5, 6, 6, 7, 8, 9]

# 刪除指定值的元素
print(l1)  # [1, 2, 12, 4, 5, 6, 6, 7, 8, 9]
l1.remove(2)  # 刪除值爲2的元素, 後邊的元素前移
print(l1)  # [1, 12, 4, 5, 6, 6, 7, 8, 9]

# 清空列表
print(l1)  # [1, 12, 4, 5, 6, 6, 7, 8, 9]
l1.clear()  # 清空列表l1
print(l1)  # []

# 列表排序
print(l2)  # [6, 7, 8, 9, 10]
l2.sort()
print(l2)  # [6, 7, 8, 9, 10]
l2.reverse()
print(l2)  # [6, 7, 8, 9, 10]

複製代碼

除了列表自帶的一些操做,還適用一些內建的函數。

# sorted 排序函數
print(l2)
l21 = sorted(l2, key=lambda x: x, reverse=False)
print(l21)

複製代碼

元組

Python 中除了列表,還有元組比較像數組。元組和列表類似,只是不能增長、刪除、修改。底層結構以下:

typedef struct {
    PyObject_VAR_HEAD
    PyObject *ob_item[1];
} PyTupleObject;
複製代碼

除了頭字段,只有一個指針數組。沒有想列表同樣的容量字段allocated,這正映了元組的不可變特性。除了元素的不可變特性外,其餘和列表同樣,是列表類型的一個子集。

你可能會有這樣的疑問,都有列表了,元組存在的意義在哪裏?元組相比於列表,有如下幾點優點:

    1. 由於元素不可變性,它能夠做爲哈希類型的 key 值。這樣使的 key 的描述意義更豐富,更易理解。
    1. 對於元組,解釋器會緩存一些小的靜態變量使用的內存,這樣在初始化時,就比列表快。

元組的初始化及經常使用操做:

# 元組的初始化
a = (1, 2, 3)
b = ('1', [2, 3])
c = ('1', '2', (3, 4))
d = ()
e = (1,)  # 元組中只有一個元素時,須要使用逗號結尾

print(a, b, c, d, e)
# (1, 2, 3) ('1', [2, 3]) ('1', '2', (3, 4)) () (1,)

# 下標獲取值
print(a[1])  # 2

# 元組合並
print(a+b)  # (1, 2, 3, '1', [2, 3])

# 內建函數使用
# 元組長度
print(len(a))  # 3

# 使用 * 是複製指針
f = a*2
print(f)  # (1, 2, 3, 1, 2, 3)
print(id(f[0]))  # 4376435920
print(id(a[0]))  # 4376435920
print(id(f[3]))  # 4376435920


# 沒法更新編輯
# a[0] = 1
# Traceback (most recent call last):
# File "/Users/deanwu/projects/01_LearnDocs/learn_codes/python/python_list.py", line 15, in <module>
# a[0] = 1
# TypeError: 'tuple' object does not support item assignment

# 沒法刪除
# del a[0]
# Traceback (most recent call last):
# File "/Users/deanwu/projects/01_LearnDocs/learn_codes/python/python_list.py", line 21, in <module>
# del a[0]
# TypeError: 'tuple' object doesn't support item deletion

複製代碼

總結

本篇咱們我學習了 Go 和 Python 的高級數據結構中類數組的結構,它們都有一些數組的特性,但又都有本身語言的特色。Go 的切片和 Python 的列表,底層都基於數組,但 Go 的切片更像是數組的描述符指針,而 Python 的列表,則是使用地址和數據分開存儲,引用地址使用連續空間存儲,繼承數組快速查找的優勢,外部存儲實現任意類型元素存儲。總體下來,甚是巧妙。

無論何種語言,咱們在使用時,既要了解結構的基本用法,還要了解其底層的邏輯結構,才能避免在使用時的一些莫名的坑。

擴展閱讀

我是DeanWu,一個努力成爲真正SRE的人。

關注公衆號「碼農吳先生」, 可第一時間獲取最新文章。回覆關鍵字「go」「python」獲取我收集的學習資料,也可回覆關鍵字「小二」,加我wx拉你進技術交流羣,聊技術聊人生~

相關文章
相關標籤/搜索