Go~Golong容器大集合, 包括Slice、Map、Heap、List、Ring

前言

  • 此篇文章,我将介绍Golong里面的容器,包括slice, map, heap,list,ring
  • 不仅介绍他的原理,也会捎带讲其原理和工作过程中遇到的坑!

Slice

切片,动态数组。

切片是Go中重要的数据类型,每个切片对象内部都维护着:数组指针、切片长度、切片容量 三个数据。

  • 一定要清楚,是数组指针!
    在这里插入图片描述
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

1.1 创建切片

// 创建切片
var nums []int

// 创建切片
var data = []int{11,22,33}
data:= []int{11,22,33}

// 创建切片
// make只用于 切片、字典、channel, 可以设置初始数据个数和容量
// 比如下面这个就表示: int切片,初始数据为1,初始容量为3
var users = make([]int,1,3)

// 切片的指针类型
var v1 = new([]int)

// 指针类型(nil)
var v2 *[]int

1.2 理解数组指针

v1 := make([]int,1,3) // 正常第一个参数len应该设置为0

fmt.Println(len(v1),cap(v1)) // 1 3

v2 := append(v1,66) // 此时V2和V1共用的一个数组指针,但是len和cap字段是各用各的

// 但是输出的时候是将按len的数据长度输出
fmt.Println(v1)  // [0 ]
fmt.Println(v2)  // [0,66]

v1[0] = 999
fmt.Println(v1)  // [999 ]
fmt.Println(v2)  // [999,66]

在这里插入图片描述

1.3 自动扩容

在向切片中追加的数据个数大于容量时,内部会自动扩容且每次都会新建数组, 扩容是当前容量的2倍(当容量超过1024时每次扩容则只增加 1/4容量)。

  • 如果初始的切片长度为3,初始容量为4,在添加一个元素之后,其长度变为4,然后容量变为4。
    在这里插入图片描述
  • 继续往切片中追加元素,切片会新创建一个数组, 其容量为旧数组的2倍, 并将旧数组中的数据拷贝到新数组中, 最后将数组指针赋值到切片中,此时len为5, 容量翻倍即变为8。
    在这里插入图片描述

1.4 字节切片

类型为byte的切片成为字节切片,可通过下面的代码创建一个字节切片:

	s := make([]byte, 0)

字节切片的操作与其他类型的切片并没有什么区别,但是在输入输出中.
(网络,文件流等)使用的非常多.

1.5 copy()函数

你可以从现有的数组中创建-一个切片,你也可以使用copy()函数复制一个切片。然而,copy()的用法可能会让你觉得非常奇怪

  • copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice
  • 两个slice可以共享同一个底层数组甚至有重叠也没有问题。使用copy()时你应小心翼翼,因为内建函数copy(dst,src)会以len(dst)和len(src)中的最小值做为复制长度。等于两个slice中较小的cap,所以我们不用担心覆盖会超出目标slice的范围。
func main() {
	a6 := []int{-10, 1, 2, 3, 4, 5}
	a4 := []int{-1, -2, -3, -4}
	fmt.Println("a6:", a6)
	fmt.Println("a4:", a4)
	copy(a6,a4) // 将a4 copy到a6
	fmt.Println("copy...")
	fmt.Println("a6:", a6)
	fmt.Println("a4:", a4)
	fmt.Println()
}

a6: [-10 1 2 3 4 5]
a4: [-1 -2 -3 -4]
copy…
a6: [-1 -2 -3 -4 4 5]
a4: [-1 -2 -3 -4]

上面的代码,我们定义了两个切片分别是a6和a4,打印之后,将a4拷贝到a6。 由于a6比a4拥有更多的元素,所以会选择a4的len作为复制长度为4,a4中所有的元素将会拷贝至a6,而a6中剩下的两个元素会保持原状。

func main() {
	b6 := []int{-10, 1, 2, 3, 4, 5}
	b4 := []int{-1, -2, -3, -4}
	fmt.Println("b6:", b6)
	fmt.Println("b4:", b4)
	copy(b4,b6)
	fmt.Println("copy")
	fmt.Println("b6:", b6)
	fmt.Println("b4:", b4)
	fmt.Println()
}

b6: [-10 1 2 3 4 5]
b4: [-1 -2 -3 -4]
copy
b6: [-10 1 2 3 4 5]
b4: [-10 1 2 3]

在这一部分代码中,由于a4的长度为4,所以b6的前四个元素才会拷贝到a4。

1.6 sort.slice()排序

sort Slice(),这个函数是在Go 1 .8中被初次引入的。这意味着sortSlice. go中的代码不能在低于1.8版本的Go环境中运行。

package main

import (
	"fmt"
	"sort"
)

type aStructure struct {
	person string
	height int
	weight int
}

func main() {
	mySlice := make([]aStructure, 0)
	mySlice = append(mySlice, aStructure{"bike", 1,2},
	                          aStructure{"faker", 100, 200},
	                          aStructure{"listen", 10, 20}) // 定义切片

	sort.Slice(mySlice, func(i, j int) bool {
		return mySlice[i].height < mySlice[j].height
	}) // 按照身高由小到大排序
	fmt.Println("<:", mySlice)
	
	sort.Slice(mySlice, func(i, j int) bool {
		return mySlice[i].height > mySlice[j].height
	})// 按照身高由大到小排序
	fmt.Println(">:",mySlice)
}

[{bike 1 2} {listen 10 20} {faker 100 200}]
[{faker 100 200} {listen 10 20} {bike 1 2}]

1.7 切片的坑

  1. 切片作为函数的形参时是传引用操作,传递的是指向切片的内存地址,这意味着在函数中对切片的任何操作都会在函数结束后体现出来。另外,函数中传递切片要比传递同样元素数量的数组高效,因为Go只是传递指向切片的内存地址,而非拷贝整个切片。但是数组在go函数中传递是数值传递,也就是会拷贝这个数组中的数据
  2. 但是切片在函数中传递一旦其len或者cap发生变化, 是不会在当前函数结束后体现出来的, 而切片的数组指针由于扩容后会有一个自动的赋值操作, 会在函数结束后体现出来
  3. 使用make初始化切片的时候,len一定要设置为0,不然0~len会有默认值出现

Map

在学习任何的编程语言时,一般都会一种数据类型称为:字典(dict)或映射(map),以键值对为元素的数据集合。

这种类型最大的特点就是查找速度非常快,因为他的底层存储是基于哈希表存储的(不同语言还会有一些差异)。

取模+拉链法来快速了解下哈希表存储原理,如下图:
请添加图片描述
这种结构之所以快,是因为根据key可以直接找到数据存放的位置;而其他的数据类型是需要从前到后去逐一比对,相对来说比较耗时。

以上只是基本的存储模型,而各个编程语言中的字典都会在此基础上进行相应的修改和优化(后续会深入讲解Golang中的map实现机制)。

Map的特点:

  • 键不能重复
  • 键必须可哈希(常见的可哈希的有:int/bool/float/string/array)
  • 无序

接下来关于map我会从两个维度来进行讲解:

  • 常见使用
  • 底层原理剖析

2.1 声明&初始化

// userInfo := map[string]string{}
userInfo := map[string]string{"name":"武沛齐","age":"18"}

userInfo["name"]  // 武沛齐
userInfo["age"] = "20"
userInfo["email"] = "wupeiqi@live.com"
// data := make(map[int]int, 10)
data := make(map[int]int)
data[100] = 998
data[200] = 999
data := make(map[string]int)
data["100"] = 998
data["200"] = 999

// 声明,nil
var row map[string]int
row = data
data := make(map[string]int)
data["100"] = 998
data["200"] = 999

// 声明,nil, new操作其实是创建了一个map指针
value := new(map[string]int)
// value["k1"] = 123  # 报错
value = &data

注意:键不重复 & 键必须可哈希(int/bool/float/string/array)

// 嵌套map初始化
v1 := make(map[[2]int]float32)
v1[[2]int{1,1}] = 1.6
v1[[2]int{1,2}] = 3.4

v2 := make(map[[2]int][3]string )
v2[[2]int{1,1}] = [3]string{"武沛齐","alex","老妖"}

2.2 常用操作

2.2.1 长度和容量

data := map[string]string{"n1":"武沛齐","n2":"alex"}
value := len(data)  // 2
// 根据参数值(10),计算出合适的容量。
// 一个map 中会包含很多桶,每个桶中可以存放8个键值对。
info := make(map[string]string, 10)

info["n1"] = "武沛齐"
info["n2"] = "alex"

v1 := len(info)  // 2
// v2 := cap(info)  // 报错, go中map不支持使用cap函数查容量

2.2.2 添加

data := map[string]string{"n1":"武沛齐","n2":"alex"}
data["n3"] = "eric"

2.2.3 修改

data := map[string]string{"n1":"武沛齐","n2":"alex"}
data["n1"] = "eric"

2…2.4 删除

data := map[string]string{"n1":"武沛齐","n2":"alex"}
delete(data,"n2")

2.2.5 查看

data := map[string]string{"n1":"武沛齐","n2":"alex"}
data["n1"]
data := map[string]string{"n1":"武沛齐","n2":"alex"}
for key,value := range data{
    fmt.Println(key,value)
}
data := map[string]string{"n1":"武沛齐","n2":"alex"}
for key := range data{
    fmt.Println(key) 
}
data := map[string]string{"n1": "武沛齐", "n2": "alex"}
for _, value := range data {
    fmt.Println(value)
}

2.2.6 嵌套

v1 := make(map[string]int)
v2 := make(map[string]string)
v3 := make(map[string]...)
v4 := make(map[string][2]int)
v5 := make(map[string][]int)
v6 := make(map[string]map[int]int)
v7 := make(map[string][2]map[string]string)
v7["n1"] = [2]map[string]string{ map[string]string{"name":"武沛齐","age":"18"},map[string]string{"name":"alex","age":"78"}}
v7["n2"] = [2]map[string]string{ map[string]string{"name":"eric","age":"18"},map[string]string{"name":"seven","age":"78"}}

// 伪代码
v7 = {
    n1:[
        {"name":"武沛齐","age":"18"},
        {"name":"alex","age":"78"}
    ],
    n2:[
        {"name":"eric","age":"18"},
        {"name":"seven","age":"78"}
    ]
}

v8 := make(map[int]int)
v9 := make(map[string]int)
v10 := make(map[float32]int)
v11 := make(map[bool]int)
v12 := make(map[ [2]int ]int)
v13 := make(map[ []int ]int) // 错误,不可哈希
v14 := make(map[ map[int]int ]int) // 错误,不可哈希
v15 := make(map[ [2][]int ]int) // 报错
v16 := make(map[ [2]map[string]string ]int) // 报错

2.2.7 变量赋值

v1 := map[string]string{"n1":"武沛齐","n2":"alex"}
v2 := v1

v1["n1"] = "wupeiqi"

ftm.Println(v1) // {"n1":"wupeiqi","n2":"alex"}
ftm.Println(v2) // {"n1":"wupeiqi","n2":"alex"}

特别提醒:无论是否存在扩容都指向同一个地址。

2.3 Map底层原理剖析

Golang中的Map有自己的一套实现原理,其核心是由hmapbmap两个结构体实现。
请添加图片描述

2.3.1 初始化

// 初始化一个可容纳10个元素的map, 但实际容量并不是10,后续会讲
info = make(map[string]string,10)

详细初始化过程

  • 第一步:创建一个hmap结构体对象。
  • 第二步:生成一个哈希因子hash0 并赋值到hmap对象中(用于后续为key创建哈希值)。
  • 第三步:根据hint=10,并根据算法规则来创建 B,当前B应该为1。

hint B
0~8 0
9~16 1
17~24 2

  • 第四步:根据B去创建去创建桶(bmap对象)并存放在buckets数组中,如果当前bmap的数量应为2.

    • 当B<4时,根据B创建桶的个数的规则为: 2 B 2^B 2B(标准桶)
    • 当B>=4时,根据B创建桶的个数的规则为: 2 B 2^B 2B + 2 B − 4 2^{B-4} 2B4(标准桶+溢出桶)

注意:每个bmap中可以存储8个键值对,当不够存储时需要使用溢出桶,并将当前bmap中的overflow字段指向溢出桶的位置。

2.3.2 写入数据

info["name"] = "武沛齐"

在map中写入数据时,内部的执行流程为:

  • 第一步:结合哈希因子和键 name生成哈希值 011011100011111110111011011
  • 第二步:获取哈希值的后B位,并根据后B的值来决定将此键值对存放到那个桶中(bmap)。

将哈希值和桶掩码(B个为1的二进制)进行 & 运算,最终得到哈希值的后B位的值。假设当B为1时,其结果为 0 :
哈希值:011011100011111110111011010
桶掩码:000000000000000000000000001
结果: 000000000000000000000000000 = 0
所以将这个数据放入第0个位置下的桶里

通过示例你会发现,找桶的原则实际上是根据后B为的位运算计算出 索引位置,然后再去buckets数组中根据索引找到目标桶(bmap)。

  • 第三步:在上一步确定桶之后,接下来就在桶中写入数据。

获取哈希值的tophash(即:哈希值的高8位),将tophash、key、value分别写入到桶中的三个数组中。
如果桶已满,则通过overflow找到溢出桶,并在溢出桶中继续写入。

注意:以后在桶中查找数据时,会基于tophash来找(tophash相同则再去比较key)。

  • 第四步:hmap的个数count++(map中的元素个数+1)

2.3.3 读取数据

value := info["name"]

在map中读取数据时,内部的执行流程为:

  • 第一步:结合哈希引子和键 name生成哈希值。
  • 第二步:获取哈希值的后B位,并根据后B位的值和桶掩码(B个为1的二进制)来决定将此键值对存放到那个桶中(bmap)。
  • 第三步:确定桶之后,再根据key的哈希值计算出tophash(高8位),根据tophash和key去桶中查找数据。

当前桶如果没找到,则根据overflow再去溢出桶中找,均未找到则表示key不存在。

2.3.5 修改数据

  • 类似查找数据, 只要在桶中找到对应的数据,就进行数据修改,找不到自然就表示添加这个数据, 如果你不想添加这个数据,也可以进行额外的操作删掉

2.3.6 扩容

在向map中添加数据时,当达到某个条件,则会引发字典扩容。

扩容条件:

  • map中数据总个数 / 桶个数 > 6.5 ,引发翻倍扩容
  • 使用了太多的溢出桶时(溢出桶使用的太多会导致map处理速度降低)。
    • B <=15,已使用的溢出桶个数 >= 2 B 2^B 2B 时,引发等量扩容
    • B > 15,已使用的溢出桶个数 >= 2 15 2^{15} 215 时,引发等量扩容
func hashGrow(t *maptype, h *hmap) {
	// If we've hit the load factor, get bigger.
	// Otherwise, there are too many overflow buckets,
	// so keep the same number of buckets and "grow" laterally.
	bigger := uint8(1)
	if !overLoadFactor(h.count+1, h.B) {
		bigger = 0
		h.flags |= sameSizeGrow
	}
	oldbuckets := h.buckets
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
	...
}

当扩容之后:

  • 第一步:B会根据扩容后新桶的个数进行增加**(翻倍扩容新B=旧B+1,等量扩容 新B=旧B)**。
  • 第二步:oldbuckets指向原来的桶(旧桶)。
  • 第三步:buckets指向新创建的桶(新桶中暂时还没有数据)。
  • 第四步:nevacuate设置为0,表示如果数据迁移的话,应该从原桶(旧桶)中的第0个位置开始迁移。
  • 第五步:noverflow设置为0,扩容后新桶中已使用的溢出桶为0。
  • 第六步:extra.oldoverflow设置为原桶(旧桶)已使用的所有溢出桶。即:h.extra.oldoverflow = h.extra.overflow
  • 第七步:extra.overflow设置为nil,因为新桶中还未使用溢出桶。
  • 第八步:extra.nextOverflow设置为新创建的桶中的第一个溢出桶的位置。
    请添加图片描述

2.3.5 迁移

扩容之后,必然要伴随着数据的迁移,即:将旧桶中的数据要迁移到新桶中。

翻倍扩容

如果是翻倍扩容,那么迁移规就是将旧桶中的数据分流至新的两个桶中(比例不定),并且桶编号的位置为:同编号位置 和 翻倍后对应编号位置
在这里插入图片描述
那么问题来了,如何实现的这种迁移呢?

首先,我们要知道如果翻倍扩容(数据总个数 / 桶个数 > 6.5),则新桶个数是旧桶的2倍,即:map中的B的值要+1(因为桶的个数等于 2 B 2^B 2B,而翻倍之后新桶的个数就是 2 B 2^B 2B * 2 ,也就是 2 B + 1 2^{B+1} 2B+1,所以 新桶的B的值=原桶B + 1 )。

迁移时会遍历某个旧桶中所有的key(包括溢出桶),并根据key重新生成哈希值,根据哈希值的 底B位 来决定将此键值对分流道那个新桶中。

在这里插入图片描述
扩容后,B的值在原来的基础上已加1,也就意味着通过多1位来计算此键值对要分流到新桶位置,如上图:

  • 当新增的位(红色)的值为 0,则数据会迁移到与旧桶编号一致的位置。
  • 当新增的位(红色)的值为 1,则数据会迁移到翻倍后对应编号位置。

旧桶个数为32个,翻倍后新桶的个数为64。
在重新计算旧桶中的所有key哈希值时,红色位只能是0或1,所以桶中的所有数据的后B位只能是以下两种情况:
- 000111【7】,意味着要迁移到与旧桶编号一致的位置。
- 100111【39】,意味着会迁移到翻倍后对应编号位置。

特别提醒:同一个桶中key的哈希值的低B位一定是相同的,不然不会放在同一个桶中,所以同一个桶中黄色标记的位都是相同的。

等量扩容

如果是等量扩容(溢出桶太多引发的扩容),那么数据迁移机制就会比较简单,就是将旧桶(含溢出桶)中的值迁移到新桶中。就是将多个桶中的数据进行紧缩,避免出现空桶空位置

这种扩容和迁移的意义在于:当溢出桶比较多而每个桶中的数据又不多时,可以通过等量扩容和迁移让数据更紧凑,从而减少溢出桶。

2.4 Map的坑

  1. Map键值为nil的坑
	info := make(map[string]string, 10)
	info[nil] = "a" // 运行才会报错
	info["n4"] = nil // 直接编译报错
	fmt.Println(info[nil])

在go语言的map中, 如果key为nil的话,编译期间不会报错,运行的时候才会报错, 但是value为nil的话会直接报错

  1. 未初始化

Go 语言中 map 和 slice 不同,map 对象必须在使用之前初始化。如果不初始化就直接赋值的话,就会出现 panic 异常

所以切记 在使用 map 这个数据类型时,一定一定要初始化,目前还没有工具可以检查是否初始化了,所以我们只能疯狂的记住它!

  1. 零值返回

从一个 nil 的 map 对象中获取值,也就是未经过初始化的map中获取值,并不会 panic,而是会得到一个零值。

  1. 并发不安全

因为 Go 内建的 map 对象并不是线程安全的,在并发读写时他会进行检查,就会出现错误!

  • 可以自己实现一个安全的加锁的map,如下:
// RWMap 一个读写锁保护的线程安全的map
type RWMap struct { 
 sync.RWMutex // 读写锁保护下面的map字段
 m map[int]int
}
// NewRWMap 新建一个RWMap
func NewRWMap(n int) *RWMap {
 return &RWMap{
  m: make(map[int]int, n),
 }
}
// Get 获取值
func (m *RWMap) Get(k int) (int, bool) {
 m.RLock()
 defer m.RUnlock()
 v, existed := m.m[k] // 在锁的保护下从map中读取
 return v, existed
}
// Set 写值
func (m *RWMap) Set(k int, v int) { 
 m.Lock()              // 锁保护
 defer m.Unlock()
 m.m[k] = v
}
// Delete 删除值
func (m *RWMap) Delete(k int) { //删除一个键
 m.Lock()                   // 锁保护
 defer m.Unlock()
 delete(m.m, k)
}
// Len 获取长度
func (m *RWMap) Len() int { // map的长度
 m.RLock()   // 锁保护
 defer m.RUnlock()
 return len(m.m)
}
// Each 循环遍历
func (m *RWMap) Each(f func(k, v int) bool) { // 遍历map
 m.RLock()             //遍历期间一直持有读锁
 defer m.RUnlock()
 for k, v := range m.m {
  if !f(k, v) {
   return
  }
 }
}

因为 Go 目前为止并不支持泛型,所以我们并不能实现一个通用的加锁 map。当然我们也可以通过 interface{} 来模拟泛型,但是成本太高,性能也会大打折扣。我们一般在开发中,对 map 对象的操作无非就是 增删改查和遍历等几种操作。

我们在并发时,只需要对他进行加读写锁,就能防止 map 并发读写报的问题!

虽然读写锁可以提供一个线程安全的 map,但是在大量并发情况下,锁的竞争会非常激烈,于是也就有了 锁是性能下降的万恶之源 的说法。

于是我们在并发编程中的原则就是:尽量少的使用锁,如果要用,尽量做到减少锁的粒度和锁的持有时间。

基于这个思路,线程安全的 map 除了我们刚才使用的加读写锁的思路,还有分片加锁,sync.Map方案

Heap

堆, 尤其大顶堆,在topK问题中还是经常使用到的, 而且它的效率是(n*log(n)), 在海量数据下求极值问题还是很友好的, 对于其基本原理我在此不多讲了,可以去学学数据结构, 在这里我主要讲go语言 container/heap包下的heap的使用

3.1 基本介绍

现在你将了解container/heap 包中提供的功能。首先,你应该知道的是container/heap实现的堆是一个树结构,树上的每个节点都是其所在子树上最小的元素。注意,我这里说的是最小的元素而不是
最小的值是为了突出堆不止支持数值。

为了使用Go语言实现堆积树,你需要自己开发一种用来比较两个元素大小的方法。这种情况下,使用Go语言中的接口来定义比较合适。

更准确地说,container/heap 包要求你实现container/heap. Interface,其定义如下:

// use heap.Push and heap.Pop.
type Interface interface {
	sort.Interface
	Push(x interface{}) // add x as element Len()
	Pop() interface{}   // remove and return element Len() - 1.
}

GO语言的接口只需要实现接口中的函数和其中组合的其他接口,比如.上面的例子中的sort. Interface、Push()函数和Pop() 函数。sort. Interface中 需要实现的函数包括Len() 、Less() 和Swap(),实现这些函数很有必要,因为实现排序的功能必须要先实现交换两个元素、计算需要排序的对象的值以及根据前面计算的值判断两元素大小这些功能。尽管你可能认为这工作量很大,但大多数情况下这些函数的实现要么很琐碎,要么很简单。

3.2 代码实现

  • 定义类型
type student struct {
	name string
	age int32
	id int64
}

type heapStudents []student
  • 实现接口
func (h *heapStudents)Push(x interface{})  {
	val, err := x.(student)
	if err != true {
		fmt.Println(fmt.Errorf("value error"))
	}
	*h = append(*h, val)// 每次push都会新开辟一个切片, 如果需要这里可以做一个优化
	heap.Init(h)
}

func (h *heapStudents) Pop() interface{} {
	old := *h
	val := old[0]
	*h = old[1:] // 每次pop都会新开辟一个切片, 如果需要这里可以做一个优化
	heap.Init(h)
	return val
}

func (h heapStudents) Len() int {
	return len(h)
}

func (h heapStudents) Less(i, j int) bool {
	//return h[i].age < h[j].age // 小顶堆, 用于升序排序
	return h[i].age > h[j].age // 大顶堆, 用于降序排序
}

func (h heapStudents) Swap(i, j int)  {
	h[i], h[j] = h[j], h[i]
}
  • 测试
func main() {
	myHeap := heapStudents{}
	myHeap = append(myHeap,
		student{"a", 6, 11},
		student{"b", 1, 11},
		student{"c", 9, 11},
		student{"d", 3, 11},
		student{"e", 5, 11},
		student{"f", 7, 11})
	fmt.Println(myHeap)

	fmt.Println("heap init...")
	myHeapInit := &myHeap
	heap.Init(myHeapInit)
	fmt.Println(myHeap)

	fmt.Println("push...")
	myHeap.Push(student{"g", 0, 11})
	myHeap.Push(student{"h", 2, 11})
	fmt.Println(myHeap)

	fmt.Println("pop...")
	newHeap := make(heapStudents, len(myHeap))
	copy(newHeap, myHeap)
	for i := 0; i < len(myHeap); i++ {
		fmt.Println(newHeap.Pop().(student))
	}
}
  • 输出

[{a 6 11} {b 1 11} {c 9 11} {d 3 11} {e 5 11} {f 7 11}]
heap init…
[{c 9 11} {e 5 11} {f 7 11} {d 3 11} {b 1 11} {a 6 11}]
push…
[{c 9 11} {e 5 11} {f 7 11} {d 3 11} {b 1 11} {a 6 11} {g 0 11} {h 2 11}]
pop…
{c 9 11}
{f 7 11}
{a 6 11}
{e 5 11}
{d 3 11}
{h 2 11}
{b 1 11}
{g 0 11}

List

链表最简单,和我们学校学习过的链表没啥俩样, 但是在Go语言中他是带傀儡节点的双向循环链表

// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {
	root Element // sentinel list element, only &root, root.prev, and root.next are used
	len  int     // current list length excluding (this) sentinel element
}

// Element is an element of a linked list.
type Element struct {
	// Next and previous pointers in the doubly-linked list of elements.
	// To simplify the implementation, internally a list l is implemented
	// as a ring, such that &l.root is both the next element of the last
	// list element (l.Back()) and the previous element of the first list
	// element (l.Front()).
	next, prev *Element

	// The list to which this element belongs.
	list *List

	// The value stored with this element.
	Value interface{}
}
  • 其实带傀儡节点和双向可以在这里看出
// Front returns the first element of list l or nil.
func (l *List) Front() *Element {
	if l.len == 0 {
		return nil
	}
	return l.root.next // 求链表第一个节点的时候返回的是root的next
}

// Back returns the last element of list l or nil.
func (l *List) Back() *Element {
	if l.len == 0 {
		return nil
	}
	return l.root.prev // 求最后一个链表节点的时候返回的是root的prev
}

所以还是希望大家看源码~

4.1 初始化

values := list.New()

4.2 基本使用

增删改查
func main() {
	values := list.New()
	e1 := values.PushBack("Two") // 链表尾部插入
	e2 := values.PushBack("Three") //
	values.PushFront("Zero") // 链表头部插入
	values.InsertBefore("One", e1) // 指定位置插入
	values.InsertAfter("Four", e2)
	values.Remove(e2) // 删除节点
	values.Remove(e2)
	values.InsertAfter("FiveFive", e2) // 此时不会插入到values链表
	values.PushBackList(values) // 链表整个插入

	printList(values) // 输出链表

	values.Init() // 清空链表
	
	fmt.Printf("After Init(): %v\n", values)
	for i := 0; i < 20; i++ {
		values.PushFront(strconv.Itoa(i)) // 因为init清空了链表,所以此时会像新建一个链表一样是一个新的链表
	}
	printList(values)

}

用list. PushBack()函数在链表尾部插入对象,也可以使用list. PushFront()函数在链表的头部插入对象。这两个函数的返回值都是链表中新插入的对象。如果你想在指定的元素后面插入新的元素,那么
可以使用list. InsertAfter()函数。类似的,你应该使用list. InsertBefore()在指定元素的前面插入新元素,如果指定的元素不存在链表就不会改变。

list. PushBackList()将一个链表的副本插入到另一个链表的后面。list. PushFrontList()函数将一个链表的副本插入到另一个链表的前面。list. Remove()函数从链表中删除一个指定的元素。

注意,使用values .Init()将会清空一个现有的链表或者初始化一个新的链表。源码里面还有一个lazyInit其表示惰性清空,如果此时没有数据进行init

// lazyInit lazily initializes a zero List value.
func (l *List) lazyInit() {
	if l.root.next == nil {
		l.Init()
	}
}
遍历方式
func printList(l *list.List) {
	fmt.Println("倒着输出....")
	for t := l.Back(); t != nil; t = t.Prev() {
		fmt.Print(t.Value, " ")
	}
	fmt.Println()
	fmt.Println("正着输出....")
	for t := l.Front(); t != nil; t = t.Next() {
		fmt.Print(t.Value, " ")
	}
	fmt.Println()
}

这里有个函数叫printlist() ,你可以传入一个list.List 变量作为指针,然后输出其中所有的内容。这段Go代码展示了如何以从头到尾和从尾到头的两个方向输出list.List 中的所有元素。 通常,在你的程序中只需要使用其中的一种方式。你可以用Prev() 和Next() 函数来反向或正向迭代链表中的元素。

Ring

Go语言代码阐述container/ring包的用法,其实就是一个链表环。其底层也是一个带傀儡节点的双向链表

type Ring struct {
	next, prev *Ring
	Value      interface{} // for use by client; untouched by this library
}

虽然这里说go语言中的list和ring的底层都是带傀儡节点的双向链表, 但那只是底层使用了相同的数据结构,它们俩个体现出来的性质或者说提供的可以使用的方法是不一样的,一个是单链表,一个是链表环

注意,container/ring 比container/list和container/heap 都简单多了,也就是说这个包中的函数比另外两个包中的要少一些。

5.1 初始化

	size := 10
	myRing := ring.New(size)

创建新的环需要使用ring. New()函数,它需要接受一个提供环的大小的参数。

5.2 基本使用

func main() {
	size := 10
	myRing := ring.New(size)
	fmt.Println("Empty ring:", *myRing)
	for i := 0; i < myRing.Len() ; i++ {
		myRing.Value = i
		myRing = myRing.Next() // 这俩步操作是在添加数据
	}
	printRing(myRing)
	myRing = myRing.Move(-1) // 向前移动一位
	printRing(myRing)

}

func printRing(r *ring.Ring)  {
	fmt.Printf("len: %d\n", r.Len())
	for i := 0; i < r.Len(); i++ {
		fmt.Print(r.Value, " ")
		r = r.Next()
	}
	fmt.Println()
}

Empty ring: {0xc04204e3c0 0xc04204e4c0 }
len: 10
0 1 2 3 4 5 6 7 8 9
len: 10
9 0 1 2 3 4 5 6 7 8

5.3 Do()

	sum := 0
	myRing.Do(func(x interface{}) {
		t := x.(int)
		sum += t
	})
	fmt.Println("Sum:", sum)

Sum: 45

ring. Do()函数可以对环上的每个元素依次调用一个函数。然而ring.Do()没有定义对环进行修改的行为。x.(int)语句称为类型断言。

// Do calls function f on each element of the ring, in forward order.
// The behavior of Do is undefined if f changes *r.
func (r *Ring) Do(f func(interface{})) {
	if r != nil {
		f(r.Value)
		for p := r.Next(); p != r; p = p.next {
			f(p.Value)
		}
	}
}

使用环会遇到的唯一的问题就是你可以无限调用ring.Next(),所以你.需要找到停下来的办法。这种情况下就需要用到ring. Len()函数。就个人而言,我比较倾向于使用ring .Do()函数来迭代环.上的所有元素,因为这样代码更简洁,但用for 循环其实也不错!

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值