自己的golang学习及速查笔记——数据结构

6. 数组

这里要注意golang里的数组是使用值传递,而不是像js那样采用引用传递的

var a = [3]int{1,2,3}
var b = [3]int{}
b = a
b[0] = 4
a[0]仍等于1

常见的数组定义1(废弃)

 1 package main
 2 import "fmt"
 3 func main() {
 
 4     这里我们创建了一个数组 a 来存放刚好 5int。元素的类型和长度都是数组类型的一部分。数组默认是零值的,对于int数组来说也就是 06     var a [5]int
 
 
 8     我们可以使用 array[index] = value 语法来设置数组指定位置的值,或者用 array[index] 得到值。
10     a[4] = 100


13     使用内置函数 len 返回数组的长度
15     fmt.Println("len:", len(a))


16     使用这个语法在一行内初始化一个数组
18     b := [5]int{1, 2, 3, 4, 5}
19     fmt.Println("dcl:", b)


20     数组的存储类型是单一的,但是你可以组合这些数据来构造多维的数据结构。
22     var twoD [2][3]int
23     for i := 0; i < 2; i++ {
24         for j := 0; j < 3; j++ {
25             twoD[i][j] = i + j
26         }
27     }
28     fmt.Println("2d: ", twoD)
29 }

常见的数组定义2(废弃)

func main () {
	// 数组声明及赋值
	var arr [4]int
	arr[0] = 2
	arr[1] = 7
	arr[2] = 11
	arr[3] = 15

	// 声明的同时赋值
	// ...表示让编译器确定数组大小
	var arr2 = [...]int {2,7,11,15}
	// or
	arr3 := [4]int {} // 默认以0填充
}

6.1 数组的声明

var a [3]int

6.2 数组的声明及初始化

数组若初始化但不生命,默认为0

var q [3]int = [3]int {1,2,3}var q = [3]int {1,2,3}
或
q := [3]int {}

若不确定数量,以…代替数量,系统会自动推断数组大小

q := [...]int {1,2,4,6,7,8}

6.3 数组的比较

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过 == 比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // 编译错误:无法比较 [2]int == [3]int

6.4 数组的遍历

var team [3]string
for k, v := range team {
    fmt.Println(k, v)
}

7. 多维数组

7.1 多维数组的声明及初始化

// 声明一个二维整型数组,两个维度分别存储 4 个元素和 2 个元素
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化外层数组中索引为 1 个和 3 的元素
array := [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化外层数组和内层数组的单个元素
array := [4][2]int{1: {0: 20}, 3: {1: 41}}

下图展示了上面示例中声明的二维数组在每次声明并初始化后包含的值。
在这里插入图片描述

7.2 同类型的数组之间可以直接赋值

var array1 [2][2]int
var array2 [2][2]int
array2 = array1
// 将 array1 的索引为 1 的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将外层数组的索引为 1、内层数组的索引为0 的整型值复制到新的整型变量里
var value int = array1[1][0]

8. 切片

https://www.cnblogs.com/liuzhongchao/p/9159896.html 这个博客讲的好
切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。

8.1 从数组或切片生成切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。

slice[开始位置:结束位置] (不包括结束位置)
var a = [3]int {1,2,3}
fmt.Println(a, a[1:2])
// [1, 2, 3] [2]

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置-开始位置。
  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取。
  • 当缺省开始位置时,表示从连续区域开头到结束位置。
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾。
  • 两者同时缺省时,与切片本身等效。
  • 两者同时为0时,等效于空切片,一般用于切片复位。

由于切片是引用类型,若想清空切片所指

a := []int {1,2,3}
fmt.Println(a[0:0])

8.2 声明新的切片

除了从原有的数组或者切片中生成切片,你也可以声明一个新的切片。
每一种类型都可以拥有其切片类型,表示多个类型元素的连续集合。因此切片类型也可以被声明。切片类型声明格式如下:

var name []type

举例:

// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty)
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty))
// 切片判定空的结果
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListEmpty == nil)

代码输出结果:

[] [] []
0 0 0
true
true
false

代码说明如下:

  • 第 2 行,声明一个字符串切片,切片中拥有多个字符串。
  • 第 5 行,声明一个整型切片,切片中拥有多个整型数值。
  • 第 8 行,将 numListEmpty 声明为一个整型切片。本来会在{}中填充切片的初始化元素,这里没有填充,所以切片是空的。但此时 numListEmpty 已经被分配了内存,但没有元素。
  • 第 11 行,切片均没有任何元素,3 个切片输出元素内容均为空。
  • 第 14 行,没有对切片进行任何操作,strList 和 numList 没有指向任何数组或者其他切片。
  • 第 17 行和第 18 行,声明但未使用的切片的默认值是 nil。strList 和 numList 也是 nil,所以和 nil 比较的结果是 true。
  • 第 19 行,numListEmpty 已经被分配到了内存,但没有元素,因此和 nil 比较时是 false。

切片是动态结构,只能与nil判定相等,不能互相判等时。

声明新的切片后,可以使用 append() 函数来添加元素。

8.3 使用make()构造切片

如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:

make([]type, size, cap)
  • type:切片的元素类型。
  • size:就是为这个类型分配多少个元素。
  • cap:预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。

示例:

a := make([]int, 2)
b := make([]int, 2, 10)

fmt.Println(a, b)
// [0 0] [0 0]
fmt.Println(len(a), len(b))
// 2 2
a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。
容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2

温馨提示
使用 make() 函数生成的切片一定发生了内存分配操作。但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

切片不一定必须经过 make() 函数才能使用。生成切片、声明后使用 append() 函数均可以正常使用切片。

8.4 append()

要注意,append返回一个新的东西,必须要再赋值

8.4.1 在尾部添加元素

var a []int
// 追加一个元素
a = append(a, 1)
// 追加多个元素
a = append(a, 1, 2, 3)
// 追加一个切片,需要手动解包,类比es6的...运算符
a = append(a, []int{1,2,3}...)

8.4.2 在头部添加元素

var a = []int{1,2,3}
// 在开头添加1个元素
a = append([]int{0}, a...)
// 在开头添加1个切片
a = append([]int{-3,-2,-1}, a...)

8.4.3 append性能分析

我们要注意切片的len和cap是两个概念,从名字上来看,len是长度,cap是容量,乍一看有一些难理解,但是只要稍微对比较偏底层的语言理解一些就很好理解。

  • 长度:就是当前切片已有的元素数量
  • 容量:当前切片在不扩容、不重新分配内存的情况下能容纳的元素数量

为什么要强调cap?因为切片在扩容的时候是按照1、2、4、8、16…这样来扩容的,每一次扩容必定会重新分配内存,相当于重新创建了一个切片,指向的地址也会改变,比较消耗时间和资源。

8.5 copy()

把sllice2的内容拷贝的slice1,返回值是成功的个数

return copy(slice1, slice2)

Go语言的内置函数 copy() 可以将一个切片的内容复制到另一个切片。
如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行复制。

与append不同的是,append要在操作结束后再赋值给原目标切片,而copy不用

slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

8.6 删除元素

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素。根据要删除元素的位置有三种情况:从开头位置删除,从中间位置删除,从尾部删除。其中删除切片尾部的元素最快。

8.6.1 从尾部删除元素

从尾部删除n个元素:

var a = []int{1,2,3,4,5}
a = a[:len(a)-N]

8.6.1 从头部删除元素

从开头删除n个元素

  • 移动头部指针:
    var a = []int{1,2,3,4,5}
    a = a[n:]
    
  • 原地完成
    var a = []int{1, 2, 3}
    a = append(a[:0], a[n:]...) // 删除开头n个元素
    
  • 利用copy
    var a = []int{1, 2, 3}
    a = a[:copy(a, a[N:])] // 删除开头n个元素
    

8.6.3 从中间删除元素

var a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素

8.6.4 提示

Go 语言中切片元素的删除过程并没有提供任何的语法糖或者方法封装,无论是初学者学习,还是实际使用都是极为麻烦的。

连续容器的元素删除无论是在任何语言中,都要将删除点前后的元素移动到新的位置。随着元素的增加,这个过程将会变得极为耗时。因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高,就需要反思是否需要更换其他的容器(如双链表等能快速从删除点删除元素)。

8.7 利用range遍历切片

当迭代切片时,关键字 range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本。

如果不需要索引值,可以使用占位字符来忽略这个值。

需要强调的是,range 创建了每个元素的副本,而不是直接返回对该元素的引用。

关键字 range 总是会从切片头部开始迭代。如果想对迭代做更多的控制,依旧可以使用传统的 for 循环,代码如下所示。

// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}
// 从第三个元素开始迭代每个元素
for index := 2; index < len(slice); index++ {
    fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}

在前面几节的学习中我们了解了两个特殊的内置函数 len 和 cap,可以用于处理数组、切片和通道。对于切片,函数 len 返回切片的长度,函数 cap 返回切片的容量。在上面的示例中,使用到了函数 len 来决定什么时候停止对切片的迭代。

9. 多维切片

我觉得这里可以类比为c++中的嵌套vector,每一层又是一个单独的切片,可以有独立的长度。

// 创建一个整型切片的切片
slice := [][]int{{10}, {100, 200}}
[
	[10],
	[100, 200]
]

在这里插入图片描述

slice[0] = append(slice[0], 20)

就变成了

[
	[10, 20],
	[100, 200]
]

10. map映射(字典)

Go语言中 map 是一种特殊的数据结构:一种元素对(pair)的无序集合,pair 的一个元素是 key,对应的另一个元素是 value,所以这个结构也称为关联数组或字典。这是一种快速寻找值的理想结构:给定 key,对应的 value 可以迅速定位。

map 这种数据结构在其他编程语言中也称为字典(Python)、hash 和 HashTable 等。

注意
map是引用类型的

定义

var varname map[keytype]valuetype
var varname map[string]int

在声明的时候不需要知道 map 的长度,map 是可以动态增长的。未初始化的 map 的值是 nil。

key 可以是任意可以用 == 或者 != 操作符比较的类型,比如 string、int、float。所以数组、切片和结构体不能作为 key,但是指针和接口类型可以。如果要用结构体作为 key 可以提供 Key() 和 Hash() 方法,这样可以通过结构体的域计算出唯一的数字或者字符串的 key。

value 可以是任意类型的;通过使用空接口类型,我们可以存储任意值,但是使用这种类型作为值时需要先做一次类型断言。

map 传递给函数的代价很小:在 32 位机器上占 4 个字节,64 位机器上占 8 个字节,无论实际上存储了多少数据。通过 key 在 map 中寻找值是很快的,比线性查找快得多,但是仍然比从数组和切片的索引中直接读取要慢 100 倍;所以如果很在乎性能的话还是建议用切片来解决问题。

map 也可以用函数作为自己的值,这样就可以用来做分支结构:key 用来选择要执行的函数。

map 是引用类型的: 内存用 make 方法来分配。

map 的初始化:var map1[keytype]valuetype = make(map[keytype]valuetype)
简写为:map1 := make(map[keytype]valuetype) 。

mapCreated := make(map[string]float)。
相当于:mapCreated := map[string]float{} 。

10.1 map的容量

和数组不同,map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。但是也可以选择标明 map 的初始容量 capacity,格式如下:

make(map[keytype]valuetype, cap)
mymap := make(map[string]float, 100)

当 map 增长到容量上限的时候,如果再增加新的 key-value 对,map 的大小会自动加 1。所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。

10.2 用切片作为 map 的值

既然一个 key 只能对应一个 value,而 value 又是一个原始类型,那么如果一个 key 要对应多个值怎么办?例如,当我们要处理 unix 机器上的所有进程,以父进程(pid 为整形)作为 key,所有的子进程(以所有子进程的 pid 组成的切片)作为 value。通过将 value 定义为 []int 类型或者其他类型的切片,就可以优雅的解决这个问题。

这里有一些定义这种 map 的例子:

mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

10.3 map的遍历

scene := make(map[string]int)
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
for key, value := range scene {
    fmt.Println(k, v)
}

只遍历key的时候可以用缺省值代替value也可以省略value

for key := range scene {
	pass
}

10.4 map的删除、清空

使用 delete() 内建函数从 map 中删除一组键值对,delete() 函数的格式如下:

delete(mapname, key)
  • mapname 为要删除的 map 实例。
  • key为要删除的 map 键值对中的键。
    清空 map 中的所有元素

有意思的是,Go 语言中并没有为 map 提供任何清空所有元素的函数、方法。清空 map 的唯一办法就是重新 make 一个新的 map。不用担心垃圾回收的效率,Go 语言中的并行垃圾回收效率比写一个清空函数高效多了。

10.5 并发环境下的mapSync

Go 语言中的 map 在并发情况下,是只读安全的,但是读写是不安全的。

需要并发读写时,一般的做法是加锁,但这样性能并不高。Go 语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map。sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

sync.Map有以下特性:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用。Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值。Range 参数中的回调函数的返回值功能是:需要继续迭代遍历时,返回 true;终止迭代遍历时,返回 false。
package main
import (
      "fmt"
      "sync"
)

func main() {
    var scene sync.Map
    // 将键值对保存到sync.Map
    scene.Store("greece", 97)
    scene.Store("london", 100)
    scene.Store("egypt", 200)
    // 从sync.Map中根据键取值
    fmt.Println(scene.Load("london"))
    // 根据键删除对应的键值对
    scene.Delete("london")
    // 遍历所有sync.Map中的键值对
    scene.Range(func(k, v interface{}) bool {
        fmt.Println("iterate:", k, v)
        return true
    })
}

sync.Map 没有提供获取 map 数量的方法,替代方法是获取时遍历自行计算数量。sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

11. list列表

列表是一种非连续存储的容器,由多个节点组成,节点通过一些变量记录彼此之间的关系。列表有多种实现方法,如单链表、双链表等。

11.1 初始化list

list 的初始化有两种方法:New 和声明。两种方法的初始化效果都是一致的。

  1. 通过 container/list 包的 New 方法初始化 list
    变量名 := list.New()
    
  2. 通过声明初始化list
    var 变量名 list.List
    

11.2 在list中插入元素

双链表支持从队列前方或后方插入元素,分别对应的方法是 PushFront 和 PushBack。

l := list.New()
l.PushBack("fist")
l.PushFront(67)

提示
这两个方法都会返回一个 *list.Element 结构。如果在以后的使用中需要删除插入的元素,则只能通过 *list.Element 配合 Remove() 方法进行删除,这种方法可以让删除更加效率化,也是双链表特性之一。

方法 功能
InsertAfter(v interface {}, mark * Element) * Element 在 mark 点之后插入元素,mark 点由其他插入函数提供
InsertBefore(v interface {}, mark * Element) *Element 在 mark 点之前插入元素,mark 点由其他插入函数提供
PushBackList(other *List) 添加 other 列表元素到尾部
PushFrontList(other *List) 添加 other 列表元素到头部

11.3 从列表中删除元素

列表的插入函数的返回值会提供一个 *list.Element 结构,这个结构记录着列表元素的值及和其他节点之间的关系等信息。从列表中删除元素时,需要用到这个结构进行快速删除。

package main
import "container/list"
func main() {
    l := list.New()
    // 尾部添加
    l.PushBack("canon")
    // 头部添加
    l.PushFront(67)
    // 尾部添加后保存元素句柄
    element := l.PushBack("fist")
    // 在fist之后添加high
    l.InsertAfter("high", element)
    // 在fist之前添加noon
    l.InsertBefore("noon", element)
    // 使用
    l.Remove(element)
}

11.4 列表的遍历

l := list.New()
for i := l.Front(); i != nil; i = i.Next() {
    fmt.Println(i.Value)
}

12. nil:空值和零值

按照 Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是 false,整型是 0,字符串是 “”,而指针,函数,interface,slice,channel 和 map 的零值都是 nil。

发布了113 篇原创文章 · 获赞 6 · 访问量 7123

猜你喜欢

转载自blog.csdn.net/swallowblank/article/details/100532636