Go基础:数据结构(定义和go语言实现)

本文最后更新于:5 个月前

前言

最近在刷leetcode的每日一题的,总会碰到一些问题,因为竞赛时用惯了c++的STL库,有数据结构的实现,可以直接使用,到现在用go总会遇到头疼的事情就是用到数据结构的时候需要自己定义结构并且实现方法,刚好这几天笔试里面也有关于数据结构的题目,所以这一篇主要来讲一下数据结构

一、数组 (Array)

数组是一种线性数据结构,它由相同类型的元素组成,每个元素可以通过下标访问。在Go语言中,数组是一种值类型,声明数组时必须指定长度,长度不能被修改。

1
2
3
4
5
// 定义一个长度为5的整型数组
var a [5]int

// 访问数组元素,下标从0开始
a[0] = 1

1、优缺点

  • 优点:
    • 数组的访问速度很快,因为它们是在内存中连续存储的
    • 数组是一种简单、清晰的数据结构,易于理解和实现
  • 缺点:
    • 数组的长度是固定的,无法动态增加或缩小
    • 插入或删除元素时需要移动数组中的其他元素,效率较低

2、适用场景和不适用场景

  • 适用场景:
    • 当需要在一个集合中存储相同类型的元素时,可以使用数组,例如存储身份证号、学号等
    • 当需要快速访问数组中的元素时,可以使用数组
  • 不适用场景:
    • 当需要动态地添加或删除元素时,不适合使用数组
    • 当需要支持不同类型的元素时,不适合使用数组

二、切片 (Slice)

切片是一个引用类型,由三个部分组成:指向底层数组的指针、切片的长度和切片的容量。切片的底层数组包含了切片中存储的所有元素,而切片则提供了访问这些元素的方法。Go 语言中的切片是动态数组,它可以根据需要动态增长和收缩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义一个长度为0的整型切片
var b []int

// 使用 make 函数初始化一个长度为5的切片
b := make([]int, 5)

// 使用切片的 append 方法向末尾添加元素
b = append(b, 1)

// 获取切片长度
l = len(b)

// 获取切片容量
c = cap(b)

// 删除指定 index 下标的元素
b = append(b[:index], b[index+1:])

// 同理删除区间[left,right] 内的元素
b = append(b[:left], b[right+1:])

1、优缺点

  • 优点:
    • 切片具有动态扩容的能力,可以在运行时根据实际需求自动增加容量,并且在传递函数参数时可以避免复制整个数组
    • 切片还具有可索引、可迭代、可排序等特性
  • 缺点:
    • 切片对内存的占用会比数组更大,因为切片会存储指向底层数组的指针、切片长度和容量等信息
    • 切片的底层数组是由 Go 运行时自动分配的,因此在内存分配和垃圾回收方面可能会对性能产生影响

2、适用场景和不适用场景

  • 适用场景:
    • 切片适用于需要动态扩容的情况,例如读取未知长度的文件或者从网络上接收数据。由于切片可以在传递函数参数时避免复制整个数组,因此它也适用于需要在函数间传递大量数据的场景
  • 不适用场景:
    • 切片不适用于需要固定长度的数组场景,例如矩阵计算、游戏开发等。由于切片可能会对性能产生影响,因此在性能要求较高的场景中也需要谨慎使用

三、链表 (Linked List)

链表由一系列节点组成,每个节点包含两部分:数据域和指针域。数据域用于存储数据,指针域用于指向下一个节点。链表分为单向链表和双向链表,单向链表的每个节点只包含指向下一个节点的指针,而双向链表的每个节点既包含指向下一个节点的指针,也包含指向上一个节点的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type ListNode struct {
Val int
Next *ListNode
}

// 在链表末尾插入一个节点
func insert(head *ListNode, val int) *ListNode {
if head == nil {
return &ListNode{val, nil}
}
node := head
for node.Next != nil {
node = node.Next
}
node.Next = &ListNode{val, nil}
return head
}

// 删除链表中的指定节点
func deleteNode(head *ListNode, val int) *ListNode {
if head == nil {
return nil
}
if head.Val == val {
return head.Next
}
node := head
for node.Next != nil {
if node.Next.Val == val {
node.Next = node.Next.Next
break
}
node = node.Next
}
return head
}

1、优缺点

  • 优点:
    • 链表可以高效地进行元素的插入和删除操作,而无需进行元素移动
  • 缺点:
    • 链表难以进行随机访问,并且需要额外的空间来存储指针

2、适用场景和不适用场景

  • 适用场景:
    • 链表适合用于需要频繁进行元素插入和删除的场景,比如实现栈、队列和哈希表等数据结构
  • 不适用场景:
    • 链表不适合用于需要进行随机访问的场景,比如需要根据下标获取元素的场景

四、栈(Stack)

栈是一种线性数据结构,具有后进先出(LIFO)的特点。栈通常有两个基本操作:push(推入)和pop(弹出)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type Stack []int

// 将元素添加到栈的顶部
func (s *Stack) Push(x int) {
*s = append(*s, x)
}
// 从栈的顶部移除元素
func (s *Stack) Pop() int {
if s.IsEmpty() {
return -1
}
x := (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
return x
}

// 栈是否为空
func (s *Stack) IsEmpty() bool {
return len(*s) == 0
}

// 栈大小
func (s *Stack) Size() int {
return len(*s)
}

// 返回栈顶元素
func (s *Stack) Top() int {
if s.IsEmpty() {
return -1
}
return (*s)[len(*s)-1]
}

1、优缺点

  • 优点:
    • 简单易懂,易于实现
    • 可以快速访问栈顶元素
    • 可以方便地实现递归算法
  • 缺点:
    • 无法随机访问其他位置的元素,只能访问栈顶元素
    • 如果栈空间不够用,需要扩容或者重新分配空间

2、适用场景和不适用场景

  • 适用场景:
    • 在编程语言中,栈用于函数调用和返回,以及表达式求值
    • 栈在实现DFS(深度优先搜索)算法、回溯算法等场景中也经常使用
  • 不适用场景:
    • 需要随机访问元素时不适合使用栈

五、队列(Queue)

队列是一种线性数据结构,它按照先进先出(FIFO)的原则存储元素,即新元素总是添加到队列的末尾,然后在队列的另一端删除旧元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Queue []int

// 将元素添加到队列的末尾
func (q *Queue) Enqueue(v int) {
*q = append(*q, v)
}
// 从队列的头部删除元素并返回
func (q *Queue) Dequeue() int {
head := (*q)[0]
*q = (*q)[1:]
return head
}

// 检查队列是否为空
func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}

1、优缺点

  • 优点:
    • 能够有效地对数据进行排序,而且处理起来非常高效,可以快速地添加或删除元素
    • 队列通常用于需要按顺序处理数据的场景,如任务调度、缓冲等
  • 缺点:
    • 只能从一端插入元素,从另一端删除元素,因此不太适合需要随机访问数据的场景

2、适用场景和不适用场景

  • 适用场景:
    • 需要按照先进先出的顺序处理数据的场景
    • 需要实现任务调度、缓冲等场景
  • 不适用场景:
    • 需要随机访问数据的场景
    • 需要在队列中间插入或删除元素的场景

六、哈希表(Hash Table)

哈希表(Hash Table)是一种基于散列函数(Hash Function)实现的数据结构,它可以提供快速的插入、查找和删除操作。哈希表的实现基于数组和链表,通过散列函数将键(Key)映射到数组索引上,将对应的值(Value)存储在对应的数组单元格中。哈希表的优点是高效,平均情况下的时间复杂度为O(1),适用于需要快速查找的场景。

1
2
3
4
5
6
7
8
9
10
11
12
// 创建map
m := make(map[string]int)

// 向map中添加元素
m["apple"] = 1
m["banana"] = 2

// 通过key来获取value
v := m["apple"]

// 判断某个key是否存在
_, ok := m["orange"]

1、优缺点

  • 优点:
    • 快速查找,平均情况下的时间复杂度为O(1),适用于需要快速查找的场景。
    • 可以动态地添加、删除元素
  • 缺点:
    • 不保证元素的顺序
    • 内存占用较高
    • 容易产生哈希冲突,冲突会导致散列表的性能下降,因此需要解决冲突的方法

2、适用场景和不适用场景

  • 适用场景:
    • 需要快速查找的场景
    • 对于缓存,数据量不大的情况下,可以使用哈希表来存储数据,方便快速查询
  • 不适用场景:
    • 如果需要有序遍历所有元素,那么哈希表不适合
    • 哈希表对内存的利用率不高,如果数据量非常大,会占用大量的内存空间,导致性能下降

七、集合(Set)

集合(Set)是一种不允许重复元素的数据结构,通常用于查找、去重、求交集、求并集等场景。集合可以用数组、链表、哈希表等数据结构实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type Set struct {
m map[interface{}]bool
}

// 创建一个新的集合
func NewSet() *Set {
return &Set{m: make(map[interface{}]bool)}
}

// 添加元素到集合中
func (s *Set) Add(item interface{}) {
s.m[item] = true
}

// 从集合中删除元素
func (s *Set) Remove(item interface{}) {
delete(s.m, item)
}

// 判断集合中是否包含某个元素
func (s *Set) Contains(item interface{}) bool {
_, ok := s.m[item]
return ok
}

// 获取集合中元素的数量
func (s *Set) Size() int {
return len(s.m)
}

// 将集合转换成切片
func (s *Set) ToList() []interface{} {
list := make([]interface{}, 0, len(s.m))
for item := range s.m {
list = append(list, item)
}
return list
}

1、优缺点

  • 优点:
    • 集合不允许重复元素,能够保证数据的唯一性
    • 集合可以进行去重、求交集、求并集等操作,方便数据处理
    • 集合的元素可以是任意类型,灵活性高
  • 缺点:
    • 集合的实现需要额外的存储空间,相比数组或链表,占用更多内存
    • 集合不支持随机访问,只能遍历或判断是否包含某个元素

2、适用场景和不适用场景

  • 适用场景:
    • 需要去重或者判断元素是否存在的场景
    • 需要求交集、求并集等操作的场景
  • 不适用场景:
    • 需要随机访问元素的场景,应该使用数组或链表
    • 需要有序访问元素的场景,应该使用链表或树结构

八、树(Tree)

树是一种非线性数据结构,它由若干个节点组成,每个节点有零个或多个子节点。树具有以下特点:

  • 树中只有一个节点,称为根节点。
  • 每个节点最多有一个父节点,除了根节点没有父节点。
  • 每个节点可以有多个子节点。
  • 如果一个节点没有子节点,则称为叶子节点。
  • 如果从一个节点可以到达另一个节点,则称这两个节点具有父子关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}

// 创建一个新的树节点
func NewTreeNode(val int, left *TreeNode, right *TreeNode) *TreeNode {
return &TreeNode{val, left, right}
}

// 插入一个节点
func (node *TreeNode) Insert(val int) *TreeNode {
if node == nil {
return NewTreeNode(val, nil, nil)
}
if val < node.Val {
node.Left = node.Left.Insert(val)
} else {
node.Right = node.Right.Insert(val)
}
return node
}

// 查找一个节点
func (node *TreeNode) Find(val int) *TreeNode {
if node == nil || node.Val == val {
return node
}
if val < node.Val {
return node.Left.Find(val)
}
return node.Right.Find(val)
}

1、优缺点

  • 优点:
    • 树是一种非常高效的数据结构,插入、查找、删除操作的时间复杂度均为O(log n)
    • 树结构天生就是具有层次性的,非常适合表示一些具有层次性质的问题,如文件系统、目录结构等
    • 树结构可以用于快速排序、二叉搜索树等算法
  • 缺点:
    • 树结构的缺点是占用空间较大
    • 树结构在处理一些非层次性问题时,效率可能不高

2、适用场景和不适用场景

  • 适用场景:
    • 表示层次性结构的问题,如文件系统、目录结构等
    • 快速排序、二叉搜索树等算法
    • 表示树形结构的问题,如语法树、家谱等
  • 不适用场景:
    • 对空间有较高要求的场景,如嵌入式系统等
    • 处理非层次性问题的场景

九、堆(Heap)

堆是一种基于完全二叉树的数据结构,其中每个节点的值都大于等于(最大堆)或小于等于(最小堆)其子节点的值。它的特点是在任何时刻,堆的根节点所存储的值都是堆中所有元素中最大或最小的。

在Go语言中,可以使用标准库中的 container/heap 包实现堆。具体实现需要实现 Heap 接口的以下方法:

  1. Len:获取堆中元素的数量;
  2. Less:比较堆中两个元素的大小;
  3. Swap:交换堆中两个元素的位置;
  4. Push:向堆中插入一个元素;
  5. Pop:从堆中弹出一个元素。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 定义一个最大堆
    type MaxHeap []int

    // 获取堆的长度
    func (h MaxHeap) Len() int { return len(h) }

    // 比较两个元素的大小
    func (h MaxHeap) Less(i, j int) bool { return h[i] > h[j] }

    // 交换两个元素的位置
    func (h MaxHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }

    // 把一个元素添加到堆中
    func (h *MaxHeap) Push(x interface{}) {
    *h = append(*h, x.(int))
    }

    // 从堆中删除一个元素
    func (h *MaxHeap) Pop() interface{} {
    n := len(*h)
    x := (*h)[n-1]
    *h = (*h)[:n-1]
    return x
    }

    在使用时,可以通过以下方式创建一个最大堆并添加元素:
1
2
3
4
5
6
7
h := &MaxHeap{}
heap.Push(h, 2)
heap.Push(h, 1)
heap.Push(h, 3)

// 获取最大值并弹出
fmt.Println(heap.Pop(h).(int)) // 3

也可以使用以下方法进行堆排序:

1
2
3
4
5
arr := []int{2, 1, 3}
h := MaxHeap(arr)
heap.Init(&h)
heap.Sort(&h)
fmt.Println(h) // [1 2 3]

1、优缺点

  • 优点:
    • 插入和删除的时间复杂度都是 O(log n),效率高
    • 堆只需要占用很小的连续内存空间,空间利用率高
  • 缺点:
    • 不能快速查找指定值,只能查找堆顶元素
    • 对于堆中相同的值,无法保证其排序前后顺序

2、适用场景和不适用场景

  • 适用场景:
    • 大/小顶堆常用于优先级队列,如任务调度等场景
    • 堆排序算法
    • 最大/最小值查询场景
  • 不适用场景:
    • 需要查找具体值的场景
    • 堆中有相同元素,需要保证顺序的场景

十、图(Graph)

图(Graph)是由节点(Vertex)和边(Edge)构成的一种数据结构,用于表示多对多的关系。节点表示实体,边表示实体之间的关系。图可以用于模拟现实生活中各种复杂的关系网络,例如社交网络、交通网络、物流网络等。

在Go语言中,可以使用邻接矩阵或邻接表来表示图。其中邻接矩阵使用二维数组表示图,邻接表使用链表或数组来表示图。具体实现方法如下:

邻接矩阵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
type Graph struct {
vertexNum int // 节点数
matrix [][]int // 邻接矩阵
visited []bool // 记录节点是否被访问过
}

func NewGraph(vertexNum int) *Graph {
matrix := make([][]int, vertexNum)
for i := range matrix {
matrix[i] = make([]int, vertexNum)
}
visited := make([]bool, vertexNum)
return &Graph{vertexNum, matrix, visited}
}

func (g *Graph) AddEdge(from, to, weight int) {
g.matrix[from][to] = weight
}

func (g *Graph) DFS(start int) {
g.visited[start] = true
fmt.Println(start)
for i := 0; i < g.vertexNum; i++ {
if !g.visited[i] && g.matrix[start][i] != 0 {
g.DFS(i)
}
}
}

func (g *Graph) BFS(start int) {
queue := []int{start}
g.visited[start] = true
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
fmt.Println(node)
for i := 0; i < g.vertexNum; i++ {
if !g.visited[i] && g.matrix[node][i] != 0 {
g.visited[i] = true
queue = append(queue, i)
}
}
}
}

邻接表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
type Graph struct {
nodes []*Node
}

type Node struct {
val int
next *Node
}

func NewNode(val int) *Node {
return &Node{val: val, next: nil}
}

func (n *Node) AddNode(val int) {
temp := n
for temp.next != nil {
temp = temp.next
}
temp.next = NewNode(val)
}

func NewGraph() *Graph {
return &Graph{nodes: []*Node{}}
}

func (g *Graph) AddNode(val int) {
g.nodes = append(g.nodes, NewNode(val))
}

func (g *Graph) AddEdge(src, dst int) {
g.nodes[src].AddNode(dst)
g.nodes[dst].AddNode(src)
}

func (g *Graph) Print() {
for i, node := range g.nodes {
fmt.Printf("Node %d: ", i)
temp := node
for temp != nil {
fmt.Printf("%d ", temp.val)
temp = temp.next
}
fmt.Printf("\n")
}
}

1、优缺点

  • 优点:
    • 图可以直观地表示复杂的关系网络
    • 可以用图算法解决许多实际问题,例如最短路径问题、最小生成树问题等
  • 缺点:
    • 图的表示和操作比较复杂,需要消耗大量的时间和空间
    • 图的算法比较复杂,需要较高的算法水平

2、适用场景和不适用场景

  • 适用场景:
    • 社交网络、交通网络、物流网络等复杂的关系网络
    • 用于最短路径问题、最小生成树问题等实际问题
  • 不适用场景:
    • 数据之间不存在明确的关系

Go基础:数据结构(定义和go语言实现)
https://gopherlinzy.github.io/2023/04/24/go-dataStructure/
作者
孙禄毅
发布于
2023年4月24日
许可协议