常见的数据结构有:数组、链表、栈、队列、图、哈希表;
1、数组
用于存储和处理一组固定大小、相同类型的数据,如存储学生成绩、数组排序等。Go 语言中的数组长度是固定的,在声明时需要指定长度。
特点:
- 数据元素类型相同:数组中的所有元素都具有相同的数据类型;
- 内存地址连续:数组在内存中是连续存储的;
- 随机访问高效:由于数组的内存地址连续,并且元素类型相同,因此可以通过索引快速访问数组中的任意元素。无论要访问数组中的第一个元素还是最后一个元素,其时间复杂度都是O(1),即访问时间与数组的大小无关。这使得数组在需要频繁随机访问数据的场景下表现出色,如查找特定位置的元素、修改指定元素的值等操作都可以快速完成。
- 大小固定:数组的大小在定义时就已经确定,一旦创建,其长度通常不能轻易改变。如果需要存储更多的数据,超过了数组的初始大小,可能需要重新分配更大的内存空间,并将原数组中的元素复制到新的数组中.`
func main() {
// 定义一个长度为5的整数数组
var arr [5]int = [5]int{1, 2, 3, 4, 5}
// 遍历数组并打印元素
for _, v := range arr {
fmt.Println(v)
}
}
时间复杂度O(1):
表示算法的执行时间与输入规模无关,是一个常数。无论输入数据的规模如何增大,算法执行所需的时间都保持不变。
2、链表
可用于实现队列、栈等数据结构,也可用于解决一些需要频繁插入和删除元素的问题,如内存管理中的空闲链表。在 Go 语言中,可以通过自定义结构体和指针来实现链表。
特点:
- 节点式存储:链表由一系列节点组成,每个节点包含数据域和指针域。数据域用于存储数据元素的值,指针域用于存储指向下一个节点的地址(在单向链表中)或指向前驱和后继节点的地址(在双向链表中)。通过指针将各个节点连接起来,形成一种链式结构。
- 内存不连续:与数组不同,链表的节点在内存中不需要连续存储。这意味着链表可以灵活地利用内存中的零散空间,只要有足够的空闲内存来分配新节点,就可以不断地向链表中添加元素,而不会受到连续内存空间大小的限制。
- 插入和删除高效:在链表中进行插入和删除操作非常方便。当需要在链表中插入一个新节点时,只需修改相关节点的指针,使其指向新插入的节点即可,不需要像数组那样移动大量的元素。
- 随机访问低效:链表不支持随机访问,即不能像数组那样通过索引直接访问任意位置的元素。要访问链表中的某个元素,必须从链表的头节点开始,沿着指针逐个节点地遍历,直到找到目标元素。平均时间复杂度为O(n),其中n是链表中节点的数量。
- 多样性:链表有多种类型,如单向链表、双向链表、循环链表等;
- 动态性:链表的大小可以根据需要动态地增长或缩小。在程序运行过程中,可以根据实际情况随时向链表中添加新节点或删除节点,这使得链表非常适合处理数据量不确定或需要频繁进行插入和删除操作的场景
package main
import (
"fmt"
)
// 定义链表节点结构体
type ListNode struct {
Val int
Next *ListNode
}
// 定义链表结构体
type LinkedList struct {
Head *ListNode
}
// 在链表末尾插入新节点
func (l *LinkedList) Insert(val int) {
newNode := &ListNode{Val: val, Next: nil}
if l.Head == nil {
l.Head = newNode
return
}
current := l.Head
for current.Next != nil {
current = current.Next
}
current.Next = newNode
}
// 查询链表中是否存在指定值的节点
func (l *LinkedList) Search(val int) bool {
current := l.Head
for current != nil {
if current.Val == val {
return true
}
current = current.Next
}
return false
}
// 删除链表中第一个值为指定值的节点
func (l *LinkedList) Delete(val int) {
if l.Head == nil {
return
}
if l.Head.Val == val {
l.Head = l.Head.Next
return
}
current := l.Head
for current.Next != nil && current.Next.Val != val {
current = current.Next
}
if current.Next != nil {
current.Next = current.Next.Next
}
}
// 打印链表中的所有节点值
func (l *LinkedList) PrintList() {
current := l.Head
for current != nil {
fmt.Print(current.Val, " ")
current = current.Next
}
fmt.Println()
}
func main() {
list := LinkedList{}
// 插入数据
list.Insert(1)
list.Insert(2)
list.Insert(3)
fmt.Print("插入数据后的链表: ")
list.PrintList()
// 查询数据
fmt.Println("是否存在值为 2 的节点:", list.Search(2))
// 删除数据
list.Delete(2)
fmt.Print("删除值为 2 的节点后的链表: ")
list.PrintList()
}
3、栈
常被用于函数调用栈,Go 语言在函数调用时会自动管理栈空间,用于存储函数的局部变量、参数等。同时,也可用于表达式求值、括号匹配等场景,通过自定义栈结构来实现。
特点:
- 后进先出(LIFO):这是栈的核心特点;
- 操作受限:栈只允许在一端进行插入和删除操作,这一端被称为栈顶。另一端则是栈底,不允许进行插入和删除操作。
- 内存管理方便:由于栈的操作主要集中在栈顶,所以在内存管理上比较方便。在程序中,栈通常用于存储函数调用的上下文、局部变量等信息。当一个函数被调用时,相关的信息会被压入栈中;函数执行完毕后,这些信息会被弹出栈,释放内存空间。
- 可以用数组或链表实现:栈可以使用数组来实现,也可以使用链表来实现。
用数组实现栈
package main
import (
"errors"
"fmt"
)
// ArrayStack 定义基于数组的栈结构体
type ArrayStack struct {
items []int
top int
}
// NewArrayStack 创建一个新的数组栈
func NewArrayStack(capacity int) *ArrayStack {
return &ArrayStack{
items: make([]int, capacity),
top: -1,
}
}
// Push 入栈操作
func (s *ArrayStack) Push(item int) error {
if s.top == len(s.items)-1 {
return errors.New("栈已满")
}
s.top++
s.items[s.top] = item
return nil
}
// Pop 出栈操作
func (s *ArrayStack) Pop() (int, error) {
if s.top == -1 {
return 0, errors.New("栈为空")
}
item := s.items[s.top]
s.top--
return item, nil
}
// Peek 查看栈顶元素
func (s *ArrayStack) Peek() (int, error) {
if s.top == -1 {
return 0, errors.New("栈为空")
}
return s.items[s.top], nil
}
// IsEmpty 判断栈是否为空
func (s *ArrayStack) IsEmpty() bool {
return s.top == -1
}
// Size 获取栈的大小
func (s *ArrayStack) Size() int {
return s.top + 1
}
单链表实现栈
// ListNode 定义链表节点结构体
type ListNode struct {
value int
next *ListNode
}
// LinkedListStack 定义基于链表的栈结构体
type LinkedListStack struct {
top *ListNode
}
// NewLinkedListStack 创建一个新的链表栈
func NewLinkedListStack() *LinkedListStack {
return &LinkedListStack{
top: nil,
}
}
// Push 入栈操作
func (s *LinkedListStack) Push(item int) {
newNode := &ListNode{
value: item,
next: s.top,
}
s.top = newNode
}
// Pop 出栈操作
func (s *LinkedListStack) Pop() (int, error) {
if s.top == nil {
return 0, errors.New("栈为空")
}
item := s.top.value
s.top = s.top.next
return item, nil
}
// Peek 查看栈顶元素
func (s *LinkedListStack) Peek() (int, error) {
if s.top == nil {
return 0, errors.New("栈为空")
}
return s.top.value, nil
}
// IsEmpty 判断栈是否为空
func (s *LinkedListStack) IsEmpty() bool {
return s.top == nil
}
// Size 获取栈的大小
func (s *LinkedListStack) Size() int {
count := 0
current := s.top
for current != nil {
count++
current = current.next
}
return count
}
func main() {
// 使用数组栈
arrayStack := NewArrayStack(5)
arrayStack.Push(1)
arrayStack.Push(2)
fmt.Println("数组栈栈顶元素:", arrayStack.Peek())
fmt.Println("数组栈出栈元素:", arrayStack.Pop())
fmt.Println("数组栈是否为空:", arrayStack.IsEmpty())
// 使用链表栈
linkedListStack := NewLinkedListStack()
linkedListStack.Push(3)
linkedListStack.Push(4)
fmt.Println("链表栈栈顶元素:", linkedListStack.Peek())
fmt.Println("链表栈出栈元素:", linkedListStack.Pop())
fmt.Println("链表栈是否为空:", linkedListStack.IsEmpty())
}
4、队列
常用于任务调度、消息队列等场景。例如,在一个 Web 服务器中,请求可以被放入队列中,然后由多个工作线程从队列中取出请求进行处理。Go 语言标准库中的container/list
包可以方便地实现队列。
特点:
- 先进先出:就像日常生活中排队一样,先进入队列的元素会先被处理,后进入的元素则需要等待。例如,在打印队列中,先提交的打印任务会先被执行,后提交的任务只能依次等待。
- 插入和删除操作的受限性:队列的插入操作(入队)只能在队列的尾部进行,而删除操作(出队)只能在队列的头部进行。这就保证了元素按照进入队列的顺序依次被处理,维护了先进先出的特性。
- 线性结构:队列中的元素之间存在着线性的顺序关系,除了队首和队尾元素外,每个元素都有唯一的前驱和后继元素。可以将队列想象成一条直线,元素在这条直线上按照顺序依次排列。
- 可以用多种数据结构实现:队列可以用数组、链表等数据结构来实现。使用数组实现的队列称为顺序队列,使用链表实现的队列称为链式队列。不同的实现方式在存储空间、操作效率等方面会有所不同,但都能满足队列的基本特性和操作要求。
package main
import (
"container/list"
"fmt"
)
var l list.List // list.List 类型的变量,是值类型
var wg sync.WaitGroup
func addToList(i int) {
defer wg.Done()
l.PushBack(i)
}
func readFromList() {
defer wg.Done()
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
}
func main() {
// 创建队列
l = list.New() // 定义的是 *list.List 类型的变量,是指针类型。
// 入队
queue.PushBack(1)
queue.PushBack(2)
queue.PushBack(3)
// 遍历队列并打印元素
for e := queue.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
// 模拟多个线程写入数据
for i := 0; i < 10; i++ {
wg.Add(1)
go addToList(i)
}
// 模拟多个线程读取数据
for i := 0; i < 5; i++ {
wg.Add(1)
go readFromList()
}
wg.Wait()
}
}
5、树
二叉搜索树可用于实现高效的查找、插入和删除操作,如数据库中的索引结构。Go 语言中可以通过自定义结构体和指针来构建二叉搜索树。此外,树还常用于文件系统的目录结构表示、XML 和 JSON 数据的解析等。
特点:
- 每个节点最多有两个子节点:二叉树中每个节点都可以有 0 个、1 个或 2 个子节点,分别称为左子节点和右子节点。
- 有序性:二叉树中的节点通常按照一定的顺序进行排列。常见的有二叉搜索树,其左子树中的所有节点的值都小于根节点的值,右子树中的所有节点的值都大于根节点的值。这种有序性使得在二叉搜索树中进行查找、插入和删除等操作具有较高的效率。
- 递归结构:二叉树具有天然的递归结构。一棵二叉树可以看作是由根节点、左子树和右子树组成,而左子树和右子树本身又是一棵二叉树。这种递归结构使得很多二叉树的操作可以通过递归算法来实现,代码简洁且易于理解。
- 层次结构:二叉树可以按照层次进行划分,根节点位于第一层,根节点的子节点位于第二层,以此类推。层次结构使得二叉树在进行层次遍历等操作时具有明确的顺序和规律。
- 节点与边的关系:对于一棵非空二叉树,其节点数
n
和边数e
之间满足关系e = n - 1
。这是因为除了根节点外,每个节点都有一条边指向它,所以边数比节点数少 1。 - 特殊的二叉树类型:存在一些特殊的二叉树,如满二叉树和完全二叉树。满二叉树是指每个节点都有两个子节点,且所有叶子节点都在同一层的二叉树;完全二叉树是指除了最后一层外,其他层的节点都是满的,且最后一层的节点是从左到右依次排列的二叉树。这些特殊类型的二叉树在某些算法和应用中具有特殊的性质和优势。
-
package main import "fmt" // 定义二叉搜索树节点结构体 type TreeNode struct { Val int Left *TreeNode Right *TreeNode } // 插入节点到二叉搜索树 func insert(root *TreeNode, val int) *TreeNode { if root == nil { return &TreeNode{Val: val} } if val < root.Val { root.Left = insert(root.Left, val) } else { root.Right = insert(root.Right, val) } return root } // 中序遍历二叉搜索树 func inorderTraversal(root *TreeNode) { if root != nil { inorderTraversal(root.Left) fmt.Println(root.Val) inorderTraversal(root.Right) } } // Search 在二叉搜索树中查找值为 val 的节点 func Search(root *TreeNode, val int) *TreeNode { if root == nil || root.Val == val { return root } if val < root.Val { return Search(root.Left, val) } return Search(root.Right, val) } // Delete 在二叉搜索树中删除值为 val 的节点 func Delete(root *TreeNode, val int) *TreeNode { if root == nil { return root } // 找到要删除的节点 if val < root.Val { root.Left = Delete(root.Left, val) } else if val > root.Val { root.Right = Delete(root.Right, val) } else { // 情况 1: 没有子节点或只有一个子节点 if root.Left == nil { return root.Right } else if root.Right == nil { return root.Left } // 情况 2: 有两个子节点 // 找到右子树中的最小节点 minNode := findMin(root.Right) root.Val = minNode.Val root.Right = Delete(root.Right, minNode.Val) } return root } // findMin 找到以 node 为根的子树中的最小节点 func findMin(node *TreeNode) *TreeNode { for node.Left != nil { node = node.Left } return node } func main() { // 创建二叉搜索树 root := &TreeNode{Val: 5} insert(root, 3) insert(root, 7) insert(root, 2) insert(root, 4) insert(root, 6) insert(root, 8) // 查询节点 target := 4 result := Search(root, target) if result != nil { fmt.Printf("找到了值为 %d 的节点\n", target) } else { fmt.Printf("未找到值为 %d 的节点\n", target) } // 中序遍历二叉搜索树 inorderTraversal(root) // 删除节点 toDelete := 3 root = Delete(root, toDelete) fmt.Print("删除节点 ", toDelete, " 后中序遍历结果: ") InorderTraversal(root) fmt.Println() }
完全二叉树(Complete Binary Tree)
- 在完全二叉树中,所有的叶节点都集中在树的最底层或者倒数第二层,并且最后一层的叶节点都尽可能地靠左排列。
- 除了最底层,其他层的节点都是满的,即除了最后一层,其他层的节点数都是满的,最后一层的节点尽可能地靠左排列。
满二叉树(Full Binary Tree)
- 在满二叉树中,每个节点要么没有子节点,要么有两个子节点,即每个节点都有0个或2个子节点。
- 满二叉树是一种特殊的完全二叉树,它的所有层都是满的,即除了最后一层,其他层的节点数都是满的。
6、哈希表
哈希表(Hash Table),也叫散列表,它可以提供快速的插入、查找和删除操作
广泛应用于缓存系统、数据查找和统计等场景。Go 语言中的map
类型就是哈希表的实现,它可以方便地存储和查找键值对数据。例如,在一个 Web 应用中,可以使用map
来缓存用户信息,提高访问效率。
特点:
- 快速查找:通过哈希函数将键值映射到固定大小的数组中。只要计算出正确的哈希值,就可以直接访问到对应的存储位置,无需像在数组或链表中那样进行线性查找。
- 高效利用空间:哈希表可以根据实际存储的数据量动态调整大小,通过负载因子来控制哈希表的空间利用率。当负载因子过高时,可以通过扩容来增加哈希表的容量。
- 灵活存储:可以存储各种类型的数据,只要能够定义合适的哈希函数来处理相应的键值。无论是整数、字符串,还是自定义的对象,都可以作为哈希表的键值进行存储。
- 无序性:哈希表中的元素是无序的,即元素的存储顺序与它们的插入顺序或键值的大小顺序无关。这是因为哈希表是通过哈希函数计算存储位置的,而不是按照某种特定的顺序排列元素。
- 键的唯一性:哈希表中的键是唯一的,如果试图插入一个已经存在的键,那么新的值会覆盖旧的值。这一特性使得哈希表非常适合用于去重以及快速查找某个键是否存在的场景。
package main
import "fmt"
func main() {
// 创建哈希表
hashMap := make(map[string]int)
// 插入键值对
hashMap["apple"] = 1
hashMap["banana"] = 2
hashMap["cherry"] = 3
// 查找键值对
fmt.Println(hashMap["apple"])
// 遍历哈希表
for key, value := range hashMap {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
if value, exists := hashTable["date"]; exists {
fmt.Println("查询到 'date' 的值为:", value)
} else {
fmt.Println("未查询到 'date'")
}
// 删除操作
delete(hashTable, "apple")
fmt.Println("删除 'apple' 后哈希表:", hashTable)
}
7、图
图是一种复杂的数据结构,用于表示各种复杂的关系,如社交网络中的人际关系、网络拓扑结构等。在 Go 语言中,可以使用邻接表或邻接矩阵来表示图,通过深度优先搜索或广度优先搜索算法来遍历图。
节点与边的关系
- 节点(顶点):是数据结构图的基本元素,代表具体的对象或实体。例如,在表示城市交通网络的图中,每个城市可以看作一个节点。
- 边:连接节点,用于表示节点之间的关系。比如在城市交通网络中,连接两个城市的道路就是边,边可以带有权重,例如表示道路的长度、通行时间等。
结构多样性
- 有向图:边具有方向,从一个顶点指向另一个顶点,这种方向性表示了关系的单向性。例如,在社交网络中,用户 A 关注用户 B,就可以用有向边来表示这种关系。
- 无向图:边没有方向,两个顶点之间的关系是对称的。如在一个表示朋友关系的图中,若 A 是 B 的朋友,那么 B 也是 A 的朋友,用无向边连接。
- 有权图:边带有权重值,这个权重可以代表各种实际意义的度量。例如在物流配送网络中,权重可以表示运输成本、距离或时间等,用于在路径规划等操作中计算最优解。
- 无权图:边不附带权重信息,仅表示节点之间存在连接关系。常用于只关注节点之间是否连通的场景,如判断网络中设备是否相互连接。
复杂的关系表示:
- 数据结构图可以表示非常复杂的关系网,一个节点可以与多个其他节点相连,形成复杂的拓扑结构。例如,在电力系统网络中,发电厂、变电站、用户等节点通过输电线路相互连接,形成一个庞大而复杂的图结构,能够准确描述电力系统中各部分之间的连接和电力传输关系。
应用广泛:
- 由于其能够灵活表示各种关系,数据结构图在众多领域都有广泛应用。在计算机科学中,用于算法设计、网络路由、图形处理等;在社会科学中,可用于分析社交网络、组织结构等;在生物学中,能用于表示蛋白质结构、基因调控网络等。
在 Go 语言中,可以通过多种方式实现图以及图的常用操作,下面分别介绍使用邻接矩阵和邻接表来表示图,并实现图的创建、添加边、广度优先搜索(BFS)、深度优先搜索(DFS)等常用操作:
package main
import (
"fmt"
)
// GraphAdjMatrix 邻接矩阵表示的图
type GraphAdjMatrix struct {
vertices int
matrix [][]int
}
// NewGraphAdjMatrix 创建一个新的邻接矩阵图
func NewGraphAdjMatrix(vertices int) *GraphAdjMatrix {
matrix := make([][]int, vertices)
for i := range matrix {
matrix[i] = make([]int, vertices)
}
return &GraphAdjMatrix{
vertices: vertices,
matrix: matrix,
}
}
// AddEdge 添加边
func (g *GraphAdjMatrix) AddEdge(u, v int) {
g.matrix[u][v] = 1
g.matrix[v][u] = 1 // 无向图
}
// BFS 广度优先搜索
func (g *GraphAdjMatrix) BFS(start int) {
visited := make([]bool, g.vertices)
queue := []int{start}
visited[start] = true
for len(queue) > 0 {
vertex := queue[0]
queue = queue[1:]
fmt.Print(vertex, " ")
for i := 0; i < g.vertices; i++ {
if g.matrix[vertex][i] == 1 &&!visited[i] {
queue = append(queue, i)
visited[i] = true
}
}
}
fmt.Println()
}
// DFS 深度优先搜索
func (g *GraphAdjMatrix) DFS(start int) {
visited := make([]bool, g.vertices)
g.dfsHelper(start, visited)
fmt.Println()
}
func (g *GraphAdjMatrix) dfsHelper(vertex int, visited []bool) {
visited[vertex] = true
fmt.Print(vertex, " ")
for i := 0; i < g.vertices; i++ {
if g.matrix[vertex][i] == 1 &&!visited[i] {
g.dfsHelper(i, visited)
}
}
}