Go基本数据结构

devtools/2024/10/21 10:12:48/

1.jdk丰富的数据结构

Jdk提供了许多基本数据结构的实现,这些数据结构是Java Collections Framework的一部分,位于java.util包中。以下是一些常见的数据结构

  1. ArrayList:一个可调整大小的数组实现,支持快速随机访问。

  2. LinkedList:一个双向链表实现,支持快速插入和删除。

  3. HashSet:基于哈希表的Set实现,不保证集合的迭代顺序。

  4. LinkedHashSet:类似于HashSet,但维护着一个运行于所有元素的双重链表。

  5. TreeSet:基于红黑树的NavigableSet实现,元素处于排序状态。

  6. HashMap:基于哈希表的Map实现,不保证映射的顺序。

  7. LinkedHashMap:类似于HashMap,维护着一个运行于所有条目的双重链表。

  8. TreeMap:基于红黑树的NavigableMap实现,键处于排序状态。

  9. PriorityQueue:基于优先级堆的无界队列,元素处于自然排序状态。

  10. ArrayDeque:一个双端队列实现,支持从两端插入和移除。

  11. ConcurrentHashMap:一个线程安全的HashMap实现。

  12. CopyOnWriteArrayList:一个线程安全的变长数组实现。

  13. CopyOnWriteArraySet:一个线程安全的Set实现。

JDK提供了如此多的数据结果,程序员只需了解每种数据结果的特点,即可应付大部分日常开发。说实在的,对于数据结构,即使经验丰富的开发(包括笔者自己),很多人都是只知其然而不知其所以然

在这里分享一个面试经历,笔者曾经一个同事,中山大学硕士生,技术杠杠的。然而在一场面试却挂了, 面试官问了一个问题,讲讲红黑树的数据结构

说这个故事不是说数据结构不重要,只是侧面说明了jdk内嵌的数据结果多么丰富,可以解决实际开发90%以上的问题。

2.Go基本数据结构

Go以简洁著称,只提供3种数据结构(后续可能会拓展,毕竟效率是第一生产力啊)

2.1.数组

数组是一个固定长度的数据类型,用于存储一段具有相同类型元素的数据块。其占有的内存是连续的,可以快速迭代数组里的每个元素。

2.1.1.申明数组

当数组初始化时,每个元素被初始化为对应的零值

// 申明包含5个元素的整形数组
var array [5]int
// 初始化为零值 [0 0 0 0 0]
fmt.Print(array) 

使用字面量声明数组

import "fmt"
func main() {// 申明一个包含5个元素的数组,并进行初始化array1 := [5]int{1,2,3,4,5}// 使用...自动计算数组长度array2 := [...]int{1,2,3,4,5,6}fmt.Println(array1)fmt.Println(array2)
}

2.1.2.基本操作

func main() {// 申明一个包含5个元素的数组,并进行初始化array := [5]int{1,2,3,4,5}// 修改索引为2的元素array[2] = 30// 遍历数组for i := 0; i < len(array); i++ {fmt.Println(array[i])}// 使用range遍历for i,value := range array {fmt.Println(i, value)}// 数组复制,只有类型及长度都一样才可进行!var array2 [5]int = arrayfmt.Println(array2)
}

2.1.3.在函数间传递数组

在函数之间传递变量时,总是以值的方式。如果数组很大,无疑开销很大。因为无论数组有多长,都会完整复制一份新的。(这一点与C语言不同,C传递数组给函数时,传递的是首元素的地址,相当于传递了指针)

import "fmt"
func main() {// 100万个元素,需要8Mvar array [1e6]int64 // 将数组复制传递给函数test(array) // 函数内部的操作不会影响到原数组fmt.Println(array[1])
}func test(a [1e6]int64) {a[1]=100fmt.Println(len(a))// 函数内部修改了元素值,操作的是复制品fmt.Println(a[1])
}

2.1.4.传递引用

为了避免耗时耗内存的复制操作,可以只传入指向数组的指针

func main() {// 100万个元素,需要8Mvar array [1e6]int64 test(&array) // 函数内部的修改,会影响原数组的内容fmt.Println(array[1])
}func test(a *[1e6]int64) {// 修改索引为1的元素的值a[1]=100fmt.Println(len(a))// 内部通过指针修改元素值fmt.Println(a[1])
}

2.2.切片

切片,实质上为动态数组,相当于java的ArrayList。这种结构可以通过append函数按需自动增长,也可以对切片再次切片来缩小一个切片的大小。

2.2.1.内部实现

切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片内部有3个字段,分别是指向数组的指针,元素个数(长度)和切片允许增长的元素个数(容量)。如下图所示:

2.2.2.创建与初始化

使用make函数创建切片

func main() {// 只指定长度,那么切片的容量等于长度slice := make([]int, 5)// 同时指定长度,容量slice2 := make([]int, 5, 10)
}

 通过切面字面量申明切片(与通过字面量申明数组非常相似,只是中括号内部少了...)

color := []string{"red", "green", "blue"}

2.2.3.nil与空切片

nil切片

有时(例如函数要求返回一个切片但发生异常),程序可能需要申明一个值为nil的切片。只需在申明时不做任何初始化。如下所示:

// 创建nil切片
var slice []int

空切片

空切片表示,在底层数组包含0个元素,也没有分配任何存储空间。使用空切片来表示空集合,例如,数组库查询返回0个查询结果。

// 使用make创建空切片
slice := make([]int,0)
// 使用字面量创建空切片
slice2 := []int{}

2.2.4.基本操作

package mainimport ("fmt""sort"
)func main() {// 声明一个整型切片var slice []int// 使用内置的make函数初始化切片slice = make([]int, 5) // 分配一个长度为5的切片,容量也为5// 使用数组初始化切片array := [5]int{1, 2, 3, 4, 5}slice = array[:5] // 创建一个切片,包含整个数组// 访问和修改元素slice[0] = 10 // 修改索引为0的元素为10fmt.Println("Modified slice:", slice)// 追加元素slice = append(slice, 6)fmt.Println("Slice after append:", slice)// 获取切片长度和容量length := len(slice)capacity := cap(slice)fmt.Println("Length:", length)fmt.Println("Capacity:", capacity)// 切片的切片subSlice := slice[1:4]fmt.Println("Sub-slice:", subSlice)// 遍历切片fmt.Println("Iterating over slice:")for index, value := range slice {fmt.Println(index, value)}// 使用for循环遍历切片fmt.Println("Iterating with for loop:")for index := 0; index < len(slice); index++ {fmt.Println(index, slice[index])}// 清空切片slice = slice[:0]fmt.Println("Slice after clearing:", slice)// 扩展切片slice = slice[:cap(slice)] // 扩展切片到其容量fmt.Println("Slice after expansion:", slice)// 截断切片slice = slice[:4] // 将切片截断到4fmt.Println("Slice after truncation:", slice)// 复制切片copySlice := append([]int(nil), slice...)fmt.Println("Copy of slice:", copySlice)// 排序切片sort.Ints(slice)fmt.Println("Sorted slice:", slice)// 删除元素slice = append(slice[:1], slice[2:]...)fmt.Println("Slice after deletion:", slice)// 反转切片for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 {slice[i], slice[j] = slice[j], slice[i]}fmt.Println("Reversed slice:", slice)
}

2.2.5.切片的切片

这里重点讲下对切片进行切片。切片之所以被称为切片,是因为创建一个新的切片就是把底层数组切出一部分。如下代码:

   // 创建一个长度与容量都是5的切片myNum := []int{10,20,30,40,50}// 创建一个新切片,长度为2,容量为4newNum := myNum[1:3]// 向第二个切片新增元素newNum = append(newNum, 60)fmt.Println(myNum)  //输出: [10 20 30 60 50]fmt.Println(newNum) //输出: [20 30 60]

执行完代码之后,我们有两个切片,它们共享同一个底层数组,但不同的切片看到同一个数组的不同部分,如下

第一个切片myNum能够看到底层数组全部5个元素,而newNum一开始只能看到索引为1和索引为2的元素,在执行append之后,可以看到新增的元素60,同时新增的元素也影响了myNum切片。从程序输出可以看出,myNum索引为3的元素从40变成60了。

2.2.6.限制再切片的容量

在创建切片的切片时,还可以指定第三个参数,用来控制新切片的容量。该参数可以更好控制追加操作,为底层数组提供了一定的保护。(为了显示行数,下面的例子使用截图而非文本)

对于第三个参数的解释

第9行代码执行 newNum   := myNum[2:3:4],这里的4代表的是原切片myNum的索引为4的位置,典型地,对于slice[i:j:k],示例 [2:3:4]

长度公式为:j - i = 3-2 = 1

容量公式为:k - i = 4-2 = 2

执行第12行,向newNum追加一个元素600,因为newNum容量为2,没有超出(此时newNum长度为2,容量为2),所以共享底层数组的myNumq切片也多了一个元素600(见第13行输出)

然而,执行第13行,向newNum继续追加一个元素700,因为超出newNum的容量,程序会创建一个新的底层数组,这个数组会将原来newNum的元素全部复制过来,新切片的容量为原切片进行翻倍。

从上面的例子可以看出,如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个append操作创建新的底层数组,从而使新切片与原切片彻底分类,这样,新切片就可以安全进行后续操作了。

2.2.7.在函数间传递切片

在函数间传递切片是以值的方式,由于切片本身属于引用类型,将切片传递给函数时,在64位架构的机器上,一个切片需要8字节的指针,8字节的长度以及8字节的容量,总共需要24字节。底层的数组不会被复制,所以在函数间传递切片非常快速。

2.3.映射

映射(map)是一种基于哈希表实现的内置数据结构,它允许存储键值对,并提供了快速的查找、插入和删除操作(相当于java的HashMap)。以下是 map 的一些核心特性和基本操作:

  • 无序:map 是无序的,不能通过索引访问,也没有固定的长度。
  • 动态:map 的大小是动态的,可以随时添加或删除键值对。
  • 唯一键:map 的键是唯一的,不允许重复。
  • 非并发安全:标准的 map 不是并发安全的,如果需要在多个 goroutine 中共享 map,应该使用 sync.Map 或者在操作 map 时使用互斥锁。

2.3.1.创建和初始化

使用make和字面量创建映射

// 创建一个映射,key为string,value为int
dict := make(map[string]int)// 创建一个映射,key,value均为string
// 使用两个键值对初始化
dict2 := map[string]string{"name":"gforgame", "url":"https://github.com/kingston-csj/gforgame"}

关于键的类型

映射的键可以是任何值,只要这个值可以使用==运算符做比较。这个值的类型可以是内置的类型,也可以是结构类型。但切片、函数这些类型由于具有引用语义,因此不能作为映射的键。

2.3.2.基本操作

package mainimport ("fmt"
)func main() {// 初始化 mapmyMap := make(map[string]int)// 赋值myMap["apple"] = 1myMap["banana"] = 2myMap["cherry"] = 3// 读取value, ok := myMap["apple"]if ok {fmt.Println("apple:", value)} else {fmt.Println("apple not found")}// 删除delete(myMap, "banana")if _, ok := myMap["banana"]; !ok {fmt.Println("banana has been deleted")}// 遍历fmt.Println("Iterating over map:")for key, value := range myMap {fmt.Println(key, value)}// 获取大小size := len(myMap)fmt.Println("Size of map:", size)// 检查键是否存在if _, exists := myMap["cherry"]; exists {fmt.Println("cherry exists in the map")} else {fmt.Println("cherry does not exist in the map")}// 预分配空间myMapPreallocated := make(map[string]int, 5)myMapPreallocated["orange"] = 5fmt.Println("Preallocated map size:", len(myMapPreallocated))// 清空 mapfor key := range myMapPreallocated {delete(myMapPreallocated, key)}fmt.Println("Map after clearing:", myMapPreallocated)
}

2.3.3.在函数间传递映射

映射是引用类型,因此在函数间传递映射时,传递的是映射的引用。这意味着,如果在函数内部修改了映射的内容,这些修改将会影响到原始的映射。

下面是一个示例,展示了如何在函数间传递映射,并在函数内部修改映射

package mainimport "fmt"// AddItem 向映射中添加一个元素
func AddItem(itemMap map[string]int, key string, value int) {itemMap[key] = value // 添加或更新键值对
}// RemoveItem 从映射中删除一个元素
func RemoveItem(itemMap map[string]int, key string) {delete(itemMap, key) // 删除键值对
}func main() {myMap := make(map[string]int)// 向映射中添加元素AddItem(myMap, "apple", 1)AddItem(myMap, "banana", 2)// 打印映射fmt.Println("After adding items:")fmt.Println(myMap) // 输出: map[apple:1 banana:2]// 从映射中删除一个元素RemoveItem(myMap, "banana")// 打印映射fmt.Println("After removing an item:")fmt.Println(myMap) // 输出: map[apple:1]
}

3.第三方数据结构

由于官方提供的数据结构确实非常少,因此社区相关的数据结构实现非常多,下面列举几个比较有名的。

  • go-datastructures  这是一个集合,包含了许多有用的、性能优异的、线程安全的 Go 数据结构。例如,增强树、位数组、队列、斐波那契堆、区间树、集合等。
  • gods  Go 数据结构库,包含链表、栈、哈希表、树等。

http://www.ppmy.cn/devtools/122914.html

相关文章

数据结构--堆的深度解析

目录 引言 一、基本概念 1.1堆的概念 1.2堆的存储结构 1.3堆的特点 二、 堆的基本操作 2.1初始化 2.2创建堆 2.3插入元素 2.4删除元素 2.5堆化操作 2.6堆的判空 2.7获取堆顶元素 三、堆的常见应用 1. 优先队列 2. 堆排序 3. Top-k 问题 4. 图论中的应用 四…

计算机毕业设计hadoop+spark天气预测 天气可视化 天气大数据 空气质量检测 空气质量分析 气象大数据 气象分析 大数据毕业设计 大数据毕设

Hadoop天气预测系统开题报告 一、研究背景与意义 在信息化和大数据时代&#xff0c;天气数据已成为社会生活和经济发展中不可或缺的重要资源。天气预测系统作为现代气象学的重要组成部分&#xff0c;对于农业生产、交通管理、环境保护以及防灾减灾等方面都具有重要意义。然而…

YOLOv8 基于NCNN的安卓部署

YOLOv8 NCNN安卓部署 前两节我们依次介绍了基于YOLOv8的剪枝和蒸馏 剪枝&#xff1a;https://blog.csdn.net/qq_41335232/article/details/140823592 蒸馏&#xff1a;https://blog.csdn.net/qq_41335232/article/details/142717122 开源代码&#xff1a;li554/yolov8_depl…

大厂面试真题:阿里经典双重检测DCL对象半初始化问题

阿里面试题中提到的双重检测DCL&#xff08;Double-Checked Locking&#xff09;对象半初始化问题&#xff0c;是Java多线程编程中一个经典的问题。以下是对这一问题的详细解析&#xff1a; 一、双重检测锁&#xff08;DCL&#xff09;概述 双重检测锁是一种用于实现单例模式…

第三十九章 创建安全对话

文章目录 第三十九章 创建安全对话概述开始安全对话 第三十九章 创建安全对话 IRIS 支持安全对话&#xff0c;遵循 WS-SecureConversation 1.3 规范。本页介绍如何手动创建安全对话。 概述 在安全对话中&#xff0c;Web 客户端向 Web 服务发出初始请求并接收包含 <Securi…

基于STM32的智能水族箱控制系统设计

引言 本项目基于STM32微控制器设计一个智能水族箱控制系统。该系统能够通过传感器监测水温、照明和水位&#xff0c;并自动控制加热器、LED灯和水泵&#xff0c;确保水族箱内的环境适宜鱼类生长。该项目展示了STM32在环境监测、设备控制和智能反馈系统中的应用。 环境准备 1…

趣味SQL | 从围棋收官到秦楚大战的数据库SQL实现(下)

目录 0 上集回顾 1 双先量化&#xff0c;得失权衡 2 各守城池&#xff0c;妥协攻守 3 SQL演算&#xff0c;三策评详 4 寸土必争&#xff0c;利益倍增 5 SQL再演&#xff0c;策略精进 6 棋道相通&#xff0c;治国有术 如果觉得本文对你有帮助&#xff0c;那么不妨也可…

树和二叉树知识点大全及相关题目练习【数据结构】

树和二叉树 要注意树和二叉树是两个完全不同的结构、概念&#xff0c;它们之间不存在包含之类的关系 树的定义 树&#xff08;Tree&#xff09;是n&#xff08;n≥0&#xff09;个结点的有限集&#xff0c;它或为空树&#xff08;n 0&#xff09;&#xff1b;或为非空树&a…