(二)Go 内建容器笔记

Go 语言数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。

注意: 在 Go 语言中,不用判断“传值”或者“传引用”, 只要看作被传递的值的类型就好了。如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。

数组

Go 语言提供了数组类型的数据结构。

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。数组容量不会发生变化。

声明数组语句:

// var variable_name [SIZE] variable_type
var balance [10] float32

初始化数组

// 初始化元素的个数不能超过 []中的数字。
var array1 = [5]float32{100.0, 2.0, 1.0, 7.0, 4.0}

// 根据 {} 内数据的数量来确认数组长度
var array2 = [...]float32{1000.0, 2.0, 3.4}

// 初始化同时给指定索引赋值,结果为 [0, 0, 1, 2, 3]
var array3 = [5] int { 2:1,3:2,4:3}

访问数组

// 通过索引来读取数组数据,索引从 0 开始
var salary float32 = balance[9] // 读取 balance 数组第 10 个数据

遍历数组

//下标遍历数组
for i := 0; i < len(array); i++ {
    fmt.Println("i=", i, array[i])
}

//range遍历数组
for k, v := range array {
    fmt.Println(k, v) // k 指数组索引,v 指索引对应的值
}

// 使用 _ 来省略索引
for _, value := range strArr {
    fmt.Println("value=", value)
}

数组比较

数组的长度、元素的类型相同的情况下,可以通过较运算符(==!=)判断数组是否相等,不能比较两个类型不同的数组,否则程序将无法完成编译。

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"

数组作为函数参数

函数传递数组参数,你需要在函数定义时,声明形参为数组:

// 函数指定数组长度
void myFunction(param [10]int)
// 函数未指定数组长度
void myFunction(param []int)
  • 未定义长度的数组只能传给不限制数组长度的函数
  • 定义了长度的数组只能传给限制了相同数组长度的函数

注意:

Go 语言的数组是值,其长度是其类型的一部分,作为函数参数时,是 值传递,函数中的修改对调用者不可见

  • [10]int[20]int 是不同类型
  • 数组作为函数参数传递的是副本,函数内修改数组并不改变原来的数组。如果想要修改,需要传入数组指针。
  • Go 语言一般不直接使用 数组,而是使用切片

切片

Go 语言切片是对数组的抽象,是对数组的一个连续片段的引用,所以切片是一个引用类型。

切片(动态数组)是 Go 语言的一种灵活,功能强悍的内置类型,长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片由三部分组成:指向底层数组的指针、len、cap。通过调用内建函数len,得到数组和切片的长度。通过调用内建函数cap,得到它们的容量。

定义切片

切片不需要说明长度

// 通过声明未指定大小数组来定义切片
var identifier []type

// 使用 make() 函数来创建切片
// make([]T, length, capacity)
var slice1 []type = make([]type, len) 
// 如果不指定 capacity,那么生成的切片的容量和长度相同,都是 len

make 函数参数

  • length 是数组的长度并且也是切片的初始长度
  • capacity (可选),指定切片容量

切片初始化

// 直接初始化切片,[] 未指定数量,表示切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s := [] int {1, 2, 3}

// 初始化切片 s ,是数组 arr 的引用
s := arr[:] 

// 从下标为 startIndex 到 endIndex-1 为切片的取值范围
s1 := s[startIndex:endIndex] 

// 使用 make 函数初始化
s :=make([]int,len,cap) 
  • 切片是可索引的,并且可以由 len() 方法获取长度。

  • 切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

注意:

一个切片在未初始化之前默认为 nil,长度为 0,也叫空(nil)切片。

切片扩容

当切片无法容纳更多的元素,Go 语言便会扩容。先生成一个容量更大的切片,把原有的元素和新元素一并拷贝到新切片中。在一般的情况下,可以认为新切片的容量是原切片容量的 2 倍。

但是,当原切片的长度大于或等于1024时,Go 语言会以原容量的1.25倍作为新容量,而不是原容量乘以2。之后,扩容的时候不断与1.25相乘,以此类推。

如果一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准,最终的新容量在很多时候都要比新容量基准更大一些。

s8 := make([]int, 10)
fmt.Printf("The capacity of s8: %d\n", cap(s8))
s8a := append(s8, make([]int, 11)...)
fmt.Printf("s8a: len: %d, cap: %d\n", len(s8a), cap(s8a))
s8b := append(s8a, make([]int, 23)...)
fmt.Printf("s8b: len: %d, cap: %d\n", len(s8b), cap(s8b))
s8c := append(s8b, make([]int, 45)...)
fmt.Printf("s8c: len: %d, cap: %d\n", len(s8c), cap(s8c))
// 执行结果
// The capacity of s8: 10
// s8a: len: 21, cap: 22
// s8b: len: 44, cap: 44
// s8c: len: 89, cap: 96

切片的底层数组永远不会被替换

在扩容的时候 Go 语言一定会生成新的底层数组,但同时生成了新的切片。它把新的切片作为了新底层数组的窗口,而没有对原切片及其底层数组做任何改动。在无需扩容时,append函数返回的是指向原底层数组的新切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片

只要长度不超过切片容量,那么使用append函数追加元素的时候就不会引起扩容。只会使切片窗口右边的(底层数组中的)元素被新的元素替换掉。

package main
import "fmt"
func main() {
	a1 := [7]int{1, 2, 3, 4, 5, 6, 7}
	fmt.Printf("a1: %v (len: %d, cap: %d)\n",
		a1, len(a1), cap(a1))
	s9 := a1[1:4]
	fmt.Printf("s9: %v (len: %d, cap: %d)\n",
		s9, len(s9), cap(s9))
    // 往切片中添加新元素
	for i := 1; i <= 5; i++ {
		s9 = append(s9, i)
        fmt.Printf("s9(%d): %v (len: %d, cap: %d)\n",
			i, s9, len(s9), cap(s9))
	}
	fmt.Printf("a1: %v (len: %d, cap: %d)\n",
		a1, len(a1), cap(a1))
	fmt.Println()
}
// 执行结果
// a1: [1 2 3 4 5 6 7] (len: 7, cap: 7)
// s9: [2 3 4] (len: 3, cap: 6)
// s9(1): [2 3 4 1] (len: 4, cap: 6)
// s9(2): [2 3 4 1 2] (len: 5, cap: 6)
// s9(3): [2 3 4 1 2 3] (len: 6, cap: 6)
// s9(4): [2 3 4 1 2 3 4] (len: 7, cap: 12)
// s9(5): [2 3 4 1 2 3 4 5] (len: 8, cap: 12)
// a1: [1 2 3 4 1 2 3] (len: 7, cap: 7)

切片操作

append() 和 copy() 函数

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。

注意: 在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。容量的扩展规律是按容量的 2 倍数进行扩充。

package main
import "fmt"
func main() {
   var numbers []int
   /* 向切片添加一个元素 */
   numbers = append(numbers, 0)
   printSlice(numbers)
    
   /* 同时添加多个元素 */
   numbers = append(numbers, 1,2,3)
   printSlice(numbers)

   /* 创建切片 numbers1 是之前切片的两倍容量*/
   numbers1 := make([]int, len(numbers), (cap(numbers))*2)

   /* 拷贝 numbers 的内容到 numbers1 */
   copy(numbers1,numbers)
   printSlice(numbers1)  
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

执行结果为

len=1 cap=1 slice=[0]
len=4 cap=4 slice=[0 1 2 3]
len=4 cap=8 slice=[0 1 2 3]

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

copy( destSlice, srcSlice []T) int

srcSlice 为数据来源切片,destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须空间足够且和目标的类型一致,copy() 函数的返回值表示实际发生复制的元素个数。

删除切片元素

从头删除

a := []int{1, 2, 3}
// 方式一
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

// 方式二
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素

// 方式三
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素

从中间删除

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个元素

从尾部删除

a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素

双向链表

Go 语言的链表实现在标准库的container/list代码包中。包中有两个公开的程序实体——ListElement,List 实现了一个双向链表(以下简称链表),而 Element 则代表了链表中元素的结构。

链表元素

链表中元素定义如下:

type Element struct {
     Value interface{}
}
func (e *Element) Next() *Element
func (e *Element) Prev() *Element

通过Value属性来获取元素的值,此外Element还有两个方法Next和Prev分别获取当前元素的前一个元素和后一个元素。

链表常用方法

// 把元素移动到另一个元素的前面和后面
func (l *List) MoveBefore(e, mark *Element)
func (l *List) MoveAfter(e, mark *Element)

// 把元素移动到链表的最前端和最后端
func (l *List) MoveToFront(e *Element)
func (l *List) MoveToBack(e *Element)

参数都是*Element类型的,*Element类型是Element类型的指针类型,*Element的值就是元素的指针。不会接受自定义的 Element类型。因为自定义Element值并不在链表中,所以无法调用,而且链表不允许我们把自己生成的Element值插入其中。

List包含的方法中,用于插入新元素的那些方法都只接受interface{}类型的值。这些方法在内部会使用Element值,包装接收到的新元素。这样做正是为了避免直接使用我们自己生成的元素,主要原因是避免链表的内部关联,遭到外界破坏。

// 于获取链表中最前端和最后端的元素
func (l *List) Front() *Element
func (l *List) Back() *Element

// 在指定的元素之前和之后插入新元素
func (l *List) InsertBefore(v interface{}, mark *Element) *Element
func (l *List) InsertAfter(v interface{}, mark *Element) *Element

// 在链表的最前端和最后端插入新元素
func (l *List) PushFront(v interface{}) *Element
func (l *List) PushBack(v interface{}) *Element

方法都返回一个Element值的指针。

ListElement都是结构体类型。结构体类型有一个特点,那就是它们的零值都会是拥有特定结构,但是没有任何定制化内容的值,相当于一个空壳。值中的字段也都会被分别赋予各自类型的零值。经过语句var l list.List声明的变量l的值是一个长度为0的链表。这个链表持有的根元素也将会是一个空壳,其中只会包含缺省的内容。可以直接使用这样的链表的。称为“开箱即用”。语句var l list.List声明的链表l可以直接使用依赖于延迟初始化

延迟初始化,你可以理解为把初始化操作延后,仅在实际需要的时候才进行。延迟初始化可以分散初始化操作带来的计算量和存储空间消耗,但频繁的判断对象是否初始化也会造成性能的浪费。

链表在删除元素、移动元素,以及一些插入元素的方法中,只要判断一下传入的元素中指向所属链表的指针,是否与当前链表的指针相等就可以了。如果不相等,就一定说明传入的元素不是这个链表中的,后续的操作就不用做了。反之,就一定说明这个链表已经被初始化了。链表的PushFront方法、PushBack方法、PushBackList方法以及PushFrontList方法总会先判断链表的状态,并在必要时进行初始化,这就是延迟初始化。

package main
import (
"fmt"
"container/list"
)

func main() {
    link := list.New()
    // 给链表添加元素。
    for i := 0; i <= 10; i++ {
        link.PushBack(i)
    }

    for p := link.Front(); p != link.Back(); p = p.Next() {
        fmt.Println("Number", p.Value)
    }
}

RingList的区别

container/ring包中的Ring类型实现的是一个循环链表。其实List在内部就是一个循环链表。它的根元素永远不会持有任何实际的元素值,而该元素的存在就是为了连接这个循环链表的首尾两端。区别:

  • 数据结构不同Ring类型的数据结构仅由它自身即可代表,而List类型则需要由它以及Element类型联合表示。
  • 表示维度上的不同:一个Ring类型的值严格来讲,只代表了其所属的循环链表中的一个元素,而一个List类型的值则代表了一个完整的链表。
  • 长度变化差异:创建并初始化一个Ring值可以指定它包含的元素的数量,循环链表一旦被创建,其长度是不可变的。
  • 初始化值不同:仅通过var r ring.Ring语句声明的r将会是一个长度为1的循环链表,而List类型的零值则是一个长度为0的链表。
  • 性能差异: Ring值的Len方法的算法复杂度是 O(N) 的,而List值的Len方法的算法复杂度则是 O(1) 的。

Map

Map 是一种无序的键值对的集合。Map 重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值,Map 是无序的,我们无法决定它的返回顺序 。

字典类型的底层原理是哈希表(hash table)。在这个实现中,键和元素的最大不同在于,前者的类型是受限的,而后者却可以是任意类型的。当要在哈希表中查找与某个键对应的值,需要先把键值作为参数传给这个哈希表。哈希表会先用哈希函数(hash function)把键值转换为哈希值。

哈希值通常是一个无符号的整数。一个哈希表会持有一定数量的桶(bucket),也可称之为哈希桶,每个哈希桶都会把自己包含的所有键的哈希值存起来。

查找时哈希表会先用键的哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中,用被查找键的哈希值与桶中哈希值逐个对比。如果一个相等的都没有,那么就说明这个桶中没有要查找的键值,这时 Go 语言就会立刻返回结果了。如果有相等的,那就再用键值本身去对比一次。由于键 - 元素对总是被捆绑在一起存储的,所以一旦找到了键,就一定能找到对应的元素值。随后,哈希表就会把相应的元素值作为结果返回。在 Go 语言的字典中,每一个键值都是由它的哈希值代表的。也就是说,字典不会独立存储任何键的值,但会独立存储它们的哈希值。

在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 键值对的数目。

Go 语言字典的键类型不可以是函数类型、字典类型和切片类型。

Map键的选择

为了提高查找效率,(求哈希和判等操作的速度越快,对应的类型就越适合作为键类型)通常用宽度小的类型作为键类型。对于布尔类型、整数类型、浮点数类型、复数类型和指针类型来说都是如此。对于字符串类型,由于它的宽度是不定的,所以要看它的值的具体长度,长度越短求哈希越快。

  • 比如,boolint8uint8类型的一个值需要占用的字节数都是1,因此这些类型的宽度就都是1

数组类型结构体类型的值求哈希实际上是依次求得它的每个元素的哈希值并进行合并,所以速度就取决于它的元素类型以及它的长度(不建议使用)。改变数组的元素值会导致哈希值发生变化,使得键失效。所以,选择键时,优先选用数值类型和指针类型,通常情况下类型的宽度越小越好。如果非要选择字符串类型的话,最好对键值的长度进行额外的约束。

字典是引用类型,所以当我们仅声明而不初始化一个字典类型的变量的时候,它的值会是nil。除了添加键 - 元素对,我们在一个值为nil的字典上做任何操作都不会引起错误。当我们试图在一个值为nil的字典中添加键 - 元素对的时候,Go 语言的运行时系统就会立即抛出一个 panic。

Map操作

Map 声明

/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)

nil map 不能用来存放键值对。

注意:可以使用 make(),但不能使用 new() 来构造 map,如果错误的使用 new()分配了一个引用对象,会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址

获取元素

map[key]
  • key 不存在时,获得 Value 类型的初始值
  • 可以使用 value, ok:= map[key] 来判断是否存在 key

Map遍历

  • 使用 range 遍历 key,或者遍历 key, value 对
  • 不保证遍历顺序,如需顺序,需要对 key 进行排序
for k, v := range map {
    fmt.Println(k, v)
}

Map删除和清空

内置函数 delete(),用于删除容器内的元素

// delete(map, 键)
scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
delete(scene, "brazil")
for k, v := range scene {
    fmt.Println(k, v)
}
// 执行结果
// route 66
// china 960

Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map。

字符串

字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。

Go语言中字符串的字节使用UTF-8编码表示Unicode文本,因此Go语言字符串是变宽字符序列,每一个字符都用一个或者多个字符表示,这跟其他的(C++,Java,Python 3)的字符串类型有着本质上的不同,后者为定宽字符序列。

Go 只有在字符串只包含7位的ASCII字符(因为它们都是用一个单一的UTF-8字节表示)时才可以被字节索引,可以将字符串转化为 Unicode 码切片( 类型为 []rune ),切片支持直接索引。

可以使用[index]方式,访问到字符串中的字符。可以访问,不可以修改。

s := "Hank"
fmt.Printf("%c", s[2])
// 返回 n

字符串可以通过 + 号进行拼接

字符串函数

字符串长度

  • 内建函数 len(),可以用来获取切片、字符串、通道(channel)等的长度。
  • Unicode 字符串长度要使用 utf8.RuneCountInString()函数。

字符串比较: 比较机制是字符的对称比较。

strings.Compare(a, b string) int
// 0,表示a == b
// -1,表示a < b
// 1,表示a > b
strings.Compare("abc", "abcd")
// 返回 1
  • strings.EqualFold(s, t string) bool

    检测字符串 s 和 t 在忽略大小写的情况下是否相等。

字符串查找

  • strings.Contains(s, substr string) bool 检测字符串 substr 是否在 s 中。
  • strings.ContainsAny(s, chars string) bool 检测字符串 chars 的中任意字符是否出现在 s 中。
  • strings.ContainsRune(s string, r rune) bool 检测 rune字符是否出现在 s 中。
  • strings.Index(s, substr string) int 返回字符串 substr 在字符串 s 中第一次出现的索引位置,若没有出现,返回-1。
strings.ContainsRune("Hank", 'a')
// 返回 true
strings.ContainsRune("Hank", 97)
// 返回 true,a的码值97

字符串统计

统计字符串 s 中非重叠substr的数量。若统计空字符串"",会返回 s 的长度加1。

strings.Count("HanZhongKang", "n")
// 返回 3
strings.Count("Hank", "")
// 返回 5,"Hank"每个rune的前后都算
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值