一,切片(可变数组)
切片是 Go 语言中一个非常重要的数据结构,它类似于数组,但是长度可以动态变化。在 Go 中,切片本质上是一个指向底层数组的指针、长度和容量。
声明切片的方式如下:
var slice []int // 声明一个空切片
slice := []int{1, 2, 3} // 声明并初始化切片
也可以使用 make 函数来创建切片:
slice := make([]int, 3) // 创建长度为3的切片
slice := make([]int, 3, 5) // 创建长度为3,容量为5的切片
其中第二个参数表示长度,第三个参数表示容量。当容量小于等于长度时,每次增加元素都会重新分配内存;当容量大于长度时,在添加元素时不需要重新分配内存。
可以使用 append 函数向切片中添加元素:
slice = append(slice, 4)
slice = append(slice, 5, 6)
如果添加的元素超过了当前容量,则会重新分配内存,并将原来的元素复制到新的数组中。
可以使用 for 循环遍历切片:
for i := range slice {
fmt.Println(slice[i])
}
也可以使用 range 关键字遍历:
for i, v := range slice {
fmt.Println(i, v)
}
对于需要删除或者插入某些元素的情况,可以使用切片的内置函数 copy 和 append:
slice1 := []int{1, 2, 3}
slice2 := []int{4, 5, 6}
copy(slice1[1:], slice2) // 将 slice2 的元素复制到 slice1 中
fmt.Println(slice1) // [1 4 5]
slice3 := append(slice1[:2], slice2...) // 在 slice1 中插入 slice2 的元素
fmt.Println(slice3) // [1 4 5 6]
切片的底层数据结构是数组,因此在传递切片时,实际上是传递了指向数组的指针。当改变切片中的元素时,底层数组也会发生相应的变化。这就意味着,在多个函数中共享同一个切片时需要注意对其进行正确地修改和使用。
二,集合(字典)
在 Go 中,字典也称为 map,是一种集合类型,它由键值对组成。每个键都唯一对应一个值,并且可以通过键来快速查找和访问相应的值。
声明和初始化 map 的方式如下:
var m1 map[string]int // 声明一个空 map
m2 := make(map[string]int) // 使用 make 函数创建 map
m3 := map[string]int{"a": 1, "b": 2, "c": 3} // 声明并初始化 map
其中第二种方式使用 make 函数创建时,需要指定其容量,例如:
m4 := make(map[string]int, 10) // 容量为10的 map
向 map 添加元素:
m := make(map[string]int)
m["a"] = 1
m["b"] = 2
m["c"] = 3
删除元素:
delete(m, "a")
查询元素:
v, ok := m["b"]
if ok {
fmt.Println(v)
}
遍历 map:
for k, v := range m {
fmt.Println(k, v)
}
注意:map 是无序的,因此每次遍历可能得到不同的顺序。
在多个 goroutine 中使用同一个 map 需要加锁保证并发安全。常见的方法有使用 sync 包提供的 Mutex 或 RWMutex 类型实现读写锁。
三,链表
在 Go 中,链表(LinkedList)是一种常见的数据结构,它由多个节点组成,每个节点包含一个值和指向下一个节点的指针。链表可以用于实现队列、栈等数据结构,也可以用于解决某些问题。
定义一个简单的链表节点:
type Node struct {
Val int
Next *Node
}
其中 Val
表示当前节点的值,Next
表示下一个节点的指针。
创建一个链表:
head := &Node{Val: 1}
node1 := &Node{Val: 2}
node2 := &Node{Val: 3}
head.Next = node1
node1.Next = node2
遍历链表:
for cur := head; cur != nil; cur = cur.Next {
fmt.Println(cur.Val)
}
插入节点:
newNode := &Node{Val: 4}
// 在第二个位置插入新节点
cur := head.Next // 找到第二个节点
newNode.Next = cur.Next // 将新节点指向原来的第三个节点
cur.Next = newNode // 将原来的第二个节点指向新节点
// 遍历输出结果:1 -> 2 -> 4 -> 3
for cur := head; cur != nil; cur = cur.Next {
fmt.Println(cur.Val)
}
删除节点:
// 删除第二个位置上的元素
cur := head.Next // 找到第二个元素
cur.Next = cur.Next.Next // 将第二个元素的指针指向第三个元素
// 遍历输出结果:1 -> 3
for cur := head; cur != nil; cur = cur.Next {
fmt.Println(cur.Val)
}
注意:链表中删除节点时,需要将要删除节点的前一个节点的指针指向要删除节点的下一个节点,否则会导致链表断裂。
四,队列
在 Go 中,队列(Queue)是一种常见的数据结构,它是一种先进先出(FIFO)的线性表。队列中元素的插入和删除操作分别在队尾和队头进行。
使用数组实现队列:
type Queue struct {
data []int
}
func (q *Queue) Enqueue(val int) {
q.data = append(q.data, val)
}
func (q *Queue) Dequeue() int {
if len(q.data) == 0 {
return -1 // 队列为空
}
val := q.data[0]
q.data = q.data[1:]
return val
}
使用链表实现队列:
type Node struct {
Val int
Next *Node
}
type Queue struct {
head *Node // 头指针,即队首节点指针
tail *Node // 尾指针,即队尾节点指针
}
func (q *Queue) Enqueue(val int) {
newNode := &Node{Val: val}
if q.head == nil && q.tail == nil { // 队列为空,新建一个节点作为头和尾
q.head = newNode
q.tail = newNode
return
}
q.tail.Next = newNode // 将新节点添加到尾部,并更新尾指针为新节点
q.tail = newNode
}
func (q *Queue) Dequeue() int {
if q.head == nil { // 队列为空,返回-1
return -1
}
val := q.head.Val
q.head = q.head.Next // 将头指针向后移动一位
if q.head == nil { // 队列只有一个元素时,更新尾指针为nil
q.tail = nil
}
return val
}
注意:队列中删除元素时,需要将头指针向后移动一位,并更新尾指针(如果队列为空,则应同时更新头和尾指针为nil)。
五,栈
在 Go 中,栈(Stack)是一种常见的数据结构,它是一种后进先出(LIFO)的线性表。栈中元素的插入和删除操作分别在栈顶进行。
使用数组实现栈:
type Stack struct {
data []int
}
func (s *Stack) Push(val int) {
s.data = append(s.data, val)
}
func (s *Stack) Pop() int {
if len(s.data) == 0 {
return -1 // 栈为空
}
val := s.data[len(s.data)-1]
s.data = s.data[:len(s.data)-1]
return val
}
使用链表实现栈:
type Node struct {
Val int
Next *Node
}
type Stack struct {
top *Node // 栈顶指针,即最新加入的节点指针
}
func (s *Stack) Push(val int) {
newNode := &Node{Val: val, Next: s.top} // 新建一个节点,并将其Next指向原来的栈顶节点
s.top = newNode // 将新节点设置为栈顶节点
}
func (s *Stack) Pop() int {
if s.top == nil { // 栈为空,返回-1
return -1
}
val := s.top.Val
s.top = s.top.Next // 将栈顶指针向下移动一位
return val
}
注意:在链表实现中,每次插入元素时都需要新建一个节点并更新top指针;而删除元素时,只需要将top指针向下移动一位即可。
六,树
在 Go 中,树(Tree)是一种非常重要的数据结构,它由若干个节点和它们之间的边组成。树的一个特点是每个节点只有一个父节点,而且没有环路。
树可以用来表示很多实际问题中的关系模型,例如文件系统、XML文档等。在算法领域中,树也被广泛应用于搜索、排序等问题中。
在 Go 中,我们可以使用结构体和指针来表示一棵树:
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
上述代码定义了一个简单的二叉树节点结构体。其中Val
表示该节点存储的值,Left
和Right
分别指向左子树和右子树。
通过递归遍历二叉树可以实现以下操作:
- 前序遍历(Preorder Traversal)
前序遍历就是先访问当前节点再依次访问左子树和右子树。具体实现如下:
func preOrder(root *TreeNode) {
if root == nil {
return
}
fmt.Println(root.Val) // 先访问当前节点
preOrder(root.Left) // 再依次访问左子树和右子树
preOrder(root.Right)
}
- 中序遍历(Inorder Traversal)
中序遍历是先访问左子树,再访问当前节点,最后访问右子树。具体实现如下:
func inOrder(root *TreeNode) {
if root == nil {
return
}
inOrder(root.Left) // 先访问左子树
fmt.Println(root.Val) // 再访问当前节点
inOrder(root.Right) // 最后访问右子树
}
- 后序遍历(Postorder Traversal)
后序遍历是先访问左子树,再访问右子树,最后访问当前节点。具体实现如下:
func postOrder(root *TreeNode) {
if root == nil {
return
}
postOrder(root.Left) // 先访问左子树和右子树
postOrder(root.Right)
fmt.Println(root.Val) // 最后访问当前节点
}
- 层序遍历(Level Order Traversal)
层序遍历就是按照从上到下、从左到右的顺序逐层遍历整棵二叉树。具体实现可以使用队列来辅助实现:
func levelOrder(root *TreeNode) [][]int {
var res [][]int
if root == nil {
return res
}
queue := []*TreeNode{root} // 初始化一个队列,并将根节点放入其中
for len(queue) > 0 { // 队列不为空时循环
size := len(queue) // 记录当前层的节点数
var curLevel []int
for i := 0; i < size; i++ {
curNode := queue[0] // 取出队列中第一个节点
queue = queue[1:] // 将队列中第一个节点弹出
curLevel = append(curLevel, curNode.Val) // 将当前节点值放入到该层的结果数组中
if curNode.Left != nil { // 如果有左子树,则将其加入到队列中
queue = append(queue, curNode.Left)
}
if curNode.Right != nil { // 如果有右子树,则将其加入到队列中
queue = append(queue, curNode.Right)
}
}
res = append(res, curLevel) // 将该层结果添加到最终结果数组中
}
return res
}
除了以上常见操作之外,还可以对树进行搜索、插入、删除等操作。总之,掌握了树的基本操作和实现方式,就能够更好地理解和使用 Go 中许多与树相关的算法和数据结构。