GO 数据结构| 青训营笔记

84 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天。

1. 字符串

  1. Go 的字符串是字节数组组成的字符序列,使用 UTF-8 编码来表示 Unicode 文本。
  2. Go 的字符串是不可变的。
  3. 双引号中的字符串是普通字符串,可以使用转义字符;反引号中的字符串保持原样。
var s1 string = "hello\n"
var s2 string = `hello\n
hello
你好!
`
fmt.Println(s1, s2)
  1. + 用于拼接字符串,若要换行,+ 需要留在上一行
{
    var s1 = "hello"
    var s2 = "world"
    var s3 = s1 + s2
    fmt.Println(s3)
}
{
    var s = "asasasasasasa" + "sdsdsdsd" + 
            "sdsdsdsd" + "sdsdsdsd"
    fmt.Println(s)
}

1.1. 字符串遍历

s := "h2世界o"
fmt.Println(len(s))                    // 9
fmt.Println(utf8.RuneCountInString(s)) // 5


for i, ch := range s {
    fmt.Println(i, ch)
}
fmt.Println("---")
// 0 104
// 1 50
// 2 19990
// 5 30028
// 8 111
// ---

for i, ch := range []byte(s) {
    fmt.Println(i, ch)
}
fmt.Println("---")
// 0 104
// 1 50
// 2 228
// 3 184
// 4 150
// 5 231
// 6 149
// 7 140
// 8 111
// ---

for i, ch := range []rune(s) {
    fmt.Println(i, ch)
}
// 0 104
// 1 50
// 2 19990
// 3 30028
// 4 111
  • func RuneCountInString(s string) (n int) 获取字符串中字符的个数
  • rune 是 int32 的别名,表示一个 Unicode 码点。
  • 字符串底层存储结构是一个 byte 数组,每个字符的 Unicode 码点将被 UTF-8 编码后存储到数组中,当遍历字符串时会将 UTF-8 值解码为 Unicode 码点赋值到 rune 类型变量中。
// 字符 '世' 的 Unicode 码点是 19990
// 字符 '世' 的 UTF-8 编码后是三字节 [228, 184, 150]
bytes := []byte{228, 184, 150}
fmt.Printf("%X\n", bytes) // E4B896

r, size := utf8.DecodeRune(bytes)
fmt.Println(r, size)  // 19990 3
fmt.Printf("%c\n", r) // 世

// 19990 是 UTF-8 解码得到的 Unicode 码点, 转为 16 进制是 4E 16
// E4B896 是 UTF-8 编码后的值, 转为 10 进制是 14,989,462

1.2. 字符串的底层结构

字符串是由一个指向底层 byte 数组的指针和记录 byte 数组长度的 int 组成的结构体。

// runtime/string.go
type stringStruct struct {
    str unsafe.Pointer
    len int
}
  • len(s) 得到的是底层字节数组的长度,而不是字符数
  • 对字符串直接使用下标访问,得到的是该处的字节
  • for-range 遍历是以字符为单位遍历的,一到多个字节被解码为 rune

2. 数组

2.1. 数组定义

// var 数组变量名 [数组大小]数据类型
var a [10]int
// [0 0 0 0 0 0 0 0 0 0]

数组定义并初始化:

var 数组变量名 [数组大小]数据类型 = [数组大小]数据类型{初始值列表}
var a [10]int = [10]int{1, 2, 3}
// [1 2 3 0 0 0 0 0 0 0]
// 当然赋值符号前的类型可以省略
var a = [10]int{1, 2, 3}
// [1 2 3 0 0 0 0 0 0 0]

// var 数组变量名 = [...]数据类型{初始值列表}
var b = [...]int{1, 2, 3}
var b [3]int = [...]int{1, 2, 3}
// [...] 表示由编译器取推断数组大小

// 按下标初始化几个元素
var c = [6]int{1: 1, 4: 4}
// [0 1 0 0 4 0]
var d = [...]int{1: 1, 4: 4}
// [0 1 0 0 4]

var 也可改为 :=

a := [10]int{1, 2, 3}
b := [...]int{1, 2, 3}
c := [6]int{1: 1, 4: 4}
d := [...]int{1: 1, 4: 4}

2.2. 数组的传递

GO 的数组是值类型,参数传递时会拷贝整个数组

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    fmt.Println(arr[1])
    fmt.Println(&arr[1])
    // 2
    // 0xc00000e398

    changeArray(arr)
    fmt.Println(arr[1])
    fmt.Println(&arr[1])
    // 2
    // 0xc00000e398
}

func changeArray(arr [5]int) {
    arr[1] = 10
    fmt.Println(arr[1])
    fmt.Println(&arr[1])
    // 10
    // 0xc00000e3c8
}

Go 的数组长度不同是不同的数据类型,函数 changeArray 只接受 5 个元素的数组

使用数组指针来模拟按引用传递:

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    fmt.Println(arr[1])
    fmt.Println(&arr[1])
    // 2
    // 0xc00000e3f8

    changeArrayByPointer(&arr)
    fmt.Println(arr[1])
    fmt.Println(&arr[1])
    // 10
    // 0xc00000e3f8
}

func changeArrayByPointer(arr *[5]int) {
    arr[1] = 10
    fmt.Println(arr[1])
    fmt.Println(&arr[1])
    // 10
    // 0xc00000e3c8
}

2.3. 数组遍历

  1. for-i

    arr := [...]int{1, 2, 3, 4, 5}
    
    for i := 0; i < len(arr); i++ {
        fmt.Println(arr[i])
    }
    // 1
    // 2
    // 3
    // 4
    // 5
    
  2. for-range

    arr := [...]int{1, 2, 3, 4, 5}
    
    for i, a := range arr {
        fmt.Println(i, arr[i], a)
    }
    // 0 1 1
    // 1 2 2
    // 2 3 3
    // 3 4 4
    // 4 5 5
    
    // 只要下标
    for i := range arr {
        fmt.Println(arr[i])
    }
    
    // 只要元素
    for _, a := range arr {
        fmt.Println(a)
    }
    

3. Slice 切片

  • 切片是数组的一个引用,切片的引用类型,传递时按引用传递
  • 切片的取值和遍历与数组一样
  • 切片的长度可动态的变化

切片的定义:

var 切片名 []数据类型

切片的创建方式一:在一个已定义的数组上建立视图

arr := [...]int{0, 1, 2, 3, 4, 5, 6}
var slice []int = arr[2:5]
fmt.Print(slice) // [2 3 4]
  • arr[2:5] 表示对数组切片,中括号内的两个数字是切片的数组下标区间,左开右闭
  • 省略其中一个表示切片到底
fmt.Print(arr[:5]) // [0 1 2 3 4]
fmt.Print(arr[2:]) // [2 3 4 5 6]
fmt.Print(arr[:])  // [0 1 2 3 4 5 6]
fmt.Print(arr[0:7]) // [0 1 2 3 4 5 6]

fmt.Print(arr[0:8]) // invalid argument: index 8 out of bounds [0:8]
  • 一个数组上可以创建多个切片
  • 切片上面还可以创建切片,切片可以向后扩展,切片区间只要不超过数组即可
arr := [...]int{0, 1, 2, 3, 4, 5, 6}
var s1 []int = arr[2:6]
var s2 []int = arr[1:3]
var ss1 []int = s1[3:5]
fmt.Print(s1, s2, ss1) // [2 3 4 5] [1 2] [5 6] 

切片的创建方式二:make

var 切片名 []数据类型 = make([]type, len, [cap])
var slice []int = make([]int, 5)
fmt.Print(slice) // [0 0 0 0 0]
  • make 可以指定切片大小 len 和容量 cap,要求 cap >= len
  • make 创建的切片底层会维护一个数组,对外不可见
var slice1 []int = make([]int, 5, 10)
fmt.Println(slice1, len(slice1), cap(slice1)) // [0 0 0 0 0] 5 10
var slice2 []int = slice1[0:10]
fmt.Println(slice2, len(slice2), cap(slice2)) // [0 0 0 0 0 0 0 0 0 0] 10 10
var slice3 []int = slice1[5:7]
fmt.Println(slice3, len(slice3), cap(slice3)) // [0 0] 2 5

切片的创建方式三:make 的语法糖

var slice []int = []int{0, 1, 2, 3, 4, 5, 6}
fmt.Println(slice, len(slice), cap(slice)) // [0 1 2 3 4 5 6] 7 7 

3.1. Slice 的底层结构

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

3.2. append 动态追加

func append(slice []Type, elems ...Type) []Type
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

s1 := arr[2:6]
s2 := s1[3:5]
fmt.Println(s1, s2, arr)
// [2 3 4 5] [5 6] [0 1 2 3 4 5 6 7]

s3 := append(s2, 10)
fmt.Println(s1, s2, s3, arr)
// [2 3 4 5] [5 6] [5 6 10] [0 1 2 3 4 5 6 10]

s4 := append(s3, 11)
fmt.Println(s1, s2, s3, s4, arr)
// [2 3 4 5] [5 6] [5 6 10] [5 6 10 11] [0 1 2 3 4 5 6 10]

s5 := append(s4, 12)
fmt.Println(s1, s2, s3, s4, s5, arr)
// [2 3 4 5] [5 6] [5 6 10] [5 6 10 11] [5 6 10 11 12] [0 1 2 3 4 5 6 10]
  • s4, s5 再次追加元素时,arr 的容量不够,会重新创建一个更大的数组,将元素拷贝过去。
var s []int
oldCap := cap(s)

for i := 1; i < 2018; i++ {
    s = append(s, i)
    if newCap := cap(s); newCap != oldCap {
        oldCap = newCap
        fmt.Printf("%d %p %p\n", newCap, s, &s)
    }
}
// cap  s 指向的底层数组  变量 s 的地址
// 1    0xc00001c480    0xc000008300
// 2    0xc00001c490    0xc000008300
// 4    0xc000014200    0xc000008300
// 8    0xc000010580    0xc000008300
// 16   0xc00008a100    0xc000008300
// 32   0xc000006500    0xc000008300
// 64   0xc00008e000    0xc000008300
// 128  0xc000090000    0xc000008300
// 256  0xc000092000    0xc000008300
// 512  0xc000094000    0xc000008300
// 848  0xc000096000    0xc000008300
// 1280 0xc0000a0000    0xc000008300
// 1792 0xc0000aa000    0xc000008300
// 2560 0xc0000b8000    0xc000008300
var s []int
fmt.Println(cap(s)) // 0

s = append(s, 1)
fmt.Println(cap(s)) // 1

s = append(s, 1, 1, 1, 1, 1)
fmt.Println(cap(s)) // 6

s = append(s, 1, 1, 1, 1, 1)
fmt.Println(cap(s)) // 12
  • slice 是其指向的底层数组的地址,&slice 是变量 slice 自身的地址
  • 当 slice 容量较小时,扩容一倍;当其容量较大时扩容大小小于一倍。(参加函数 growslice
  • 当一次 append 多个元素时,若期望容量大于上一条的容量,则直接扩容到期望容量
  • 扩容时是并发不安全的

3.3. copy 拷贝切片

func copy(dst, src []Type) int
var s1 []int = []int{1, 2, 3, 4}
var s2 []int = make([]int, 10)
var s3 []int = make([]int, 2)

copy(s2, s1)
copy(s3, s1)
fmt.Println(s1, s2, s3)
// [1 2 3 4] [1 2 3 4 0 0 0 0 0 0] [1 2]

s2[0] = 9
fmt.Println(s1, s2, s3)
// [1 2 3 4] [9 2 3 4 0 0 0 0 0 0] [1 2]

4. Map

Map 的定义:

var 变量名 map[key类型]value类型

Map 的创建:make

变量名 = make(t Type, size)
var m map[string]string
m = make(map[string]string, 16)

make 的语法糖

var m map[string]string = map[string]string{
    "name": "A",
    "age": "1",
}
  • map 在使用前一定要 make
  • map 的键值对是无序的
  • 当 key 不存在时,获取的是 value 类型的初始值
  • map 是引用类型

4.1. map 的操作

var m map[string]string = map[string]string{
    "name": "A",
    "age":  "1",
}
fmt.Println(m) // map[age:1 name:A]

// 更新
fmt.Println(m["name"]) // A
m["name"] = "B"
fmt.Println(m["name"]) // B

// 增加
fmt.Println(m["other"]) //
m["other"] = "O"
fmt.Println(m["other"]) // O

// 删除
fmt.Println(m["age"]) // 1
delete(m, "age")
fmt.Println(m["age"]) //

// 查找
v1, v1Exist := m["name"]
v2, v2Exist := m["age"]
fmt.Println(v1Exist, v1) // true B
fmt.Println(v2Exist, v2) // false 

4.2. map 遍历

m := map[string]string{
    "a": "A",
    "b": "B",
    "c": "C",
}

for k, v := range m {
    fmt.Println(k, v)
}
// a A
// b B
// c C

4.3. k v 的类型

可以作为 key 的类型:

  1. 必须可以比较相等
  2. 除了 slice, map, function 其他内建类型都可作为 key
  3. Struct 类型不包含 slice, map, function 字段,也可作为 key

4.4. map 例题:最长不含重复字符的子字符串

剑指 Offer 48. 最长不含重复字符的子字符串

// 设以 s[i] 结尾的 s 的子串中, 无重复字符且最长的字符串的长度为 f[i]
// 
// f[0] = 1
// f[i] = f[i-1] + 1,  if (s[i] not in s[0:i-1])
// f[i] = i - r,  if (s[i] in s[0:i-1]), r 是最近的一个重复的 s[i] 的下标
// res = max(f)

func lengthOfLongestSubstring(s string) int {
    var f = 0
    var ans = 0

    // m 中记录了 s[i] 之前区间出现的所有的 <字符, 下标>
    // 若某个字符出现了两次, m 中总是记录离 s[i] 最近的下标
    m := make(map[rune]int)

    for i, ch := range []rune(s) {
        // 如果存在重复字符, 且重复的字符在当前 l 的区间内
        if r, exist := m[ch]; exist && f >= i - r {
            f = i - r
        } else {
            f = f + 1
        }

        // 添加或更新当前字符位置
        m[ch] = i

        if ans < f {
            ans = f
        }
    }

    return ans
}

4.5. Map 的底层结构

// runtime/map.go

// A header for a Go map.
type hmap struct {
    // Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
    // Make sure this stays in sync with the compiler's definition.
    count     int // # live cells == size of map.  Must be first (used by len() builtin)
    flags     uint8
    B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
    noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
    hash0     uint32 // hash seed

    buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
    oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
    nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

    extra *mapextra // optional fields
}

// A bucket for a Go map.
type bmap struct {
    // tophash generally contains the top byte of the hash value
    // for each key in this bucket. If tophash[0] < minTopHash,
    // tophash[0] is a bucket evacuation state instead.
    tophash [bucketCnt]uint8
    // Followed by bucketCnt keys and then bucketCnt elems.
    // NOTE: packing all the keys together and then all the elems together makes the
    // code a bit more complicated than alternating key/elem/key/elem/... but it allows
    // us to eliminate padding which would be needed for, e.g., map[int64]int8.
    // Followed by an overflow pointer.
}

const (
    // Maximum number of key/elem pairs a bucket can hold.
    bucketCntBits = 3
    bucketCnt     = 1 << bucketCntBits  // 即 8
)
  • buckets 指向的是一个长度为 2^Bbmap 数组
  • bmap.tophash 是一个长度为 8uint8 数组,存放了 K 的哈希值高 8 位。
    其后还有 8 个 key 和 8 个 value,即一个 bmap 能存放 8 个键值对
    以及一个 overflow pointer(溢出指针),当键值对超过 8 个时,该指针指向下一个 bmap
    由于无法确定 K V 的类型,就无法确定大小,因此这些字段将在编译时才添加。
  • map 的读写是有并发问题的

sync.Map 是并发安全的

  • 使用了两个 map,分离了扩容
  • 不会引发扩容的操作,如查、改,使用 read map
  • 可能引发扩容的操作,如新增,使用 dirty map