1.jdk丰富的数据结构
Jdk提供了许多基本数据结构的实现,这些数据结构是Java Collections Framework的一部分,位于java.util
包中。以下是一些常见的数据结构:
-
ArrayList:一个可调整大小的数组实现,支持快速随机访问。
-
LinkedList:一个双向链表实现,支持快速插入和删除。
-
HashSet:基于哈希表的Set实现,不保证集合的迭代顺序。
-
LinkedHashSet:类似于HashSet,但维护着一个运行于所有元素的双重链表。
-
TreeSet:基于红黑树的NavigableSet实现,元素处于排序状态。
-
HashMap:基于哈希表的Map实现,不保证映射的顺序。
-
LinkedHashMap:类似于HashMap,维护着一个运行于所有条目的双重链表。
-
TreeMap:基于红黑树的NavigableMap实现,键处于排序状态。
-
PriorityQueue:基于优先级堆的无界队列,元素处于自然排序状态。
-
ArrayDeque:一个双端队列实现,支持从两端插入和移除。
-
ConcurrentHashMap:一个线程安全的HashMap实现。
-
CopyOnWriteArrayList:一个线程安全的变长数组实现。
-
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 = array
fmt.Println(array2)
}
2.1.3.在函数间传递数组
在函数之间传递变量时,总是以值的方式。如果数组很大,无疑开销很大。因为无论数组有多长,都会完整复制一份新的。(这一点与C语言不同,C传递数组给函数时,传递的是首元素的地址,相当于传递了指针)
import "fmt"
func main() {
// 100万个元素,需要8M
var array [1e6]int64
// 将数组复制传递给函数
test(array)
// 函数内部的操作不会影响到原数组
fmt.Println(array[1])
}
func test(a [1e6]int64) {
a[1]=100
fmt.Println(len(a))
// 函数内部修改了元素值,操作的是复制品
fmt.Println(a[1])
}
2.1.4.传递引用
为了避免耗时耗内存的复制操作,可以只传入指向数组的指针
func main() {
// 100万个元素,需要8M
var array [1e6]int64
test(&array)
// 函数内部的修改,会影响原数组的内容
fmt.Println(array[1])
}
func test(a *[1e6]int64) {
// 修改索引为1的元素的值
a[1]=100
fmt.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 main
import (
"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的元素为10
fmt.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] // 将切片截断到4
fmt.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,容量为4
newNum := 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 main
import (
"fmt"
)
func main() {
// 初始化 map
myMap := make(map[string]int)
// 赋值
myMap["apple"] = 1
myMap["banana"] = 2
myMap["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"] = 5
fmt.Println("Preallocated map size:", len(myMapPreallocated))
// 清空 map
for key := range myMapPreallocated {
delete(myMapPreallocated, key)
}
fmt.Println("Map after clearing:", myMapPreallocated)
}
2.3.3.在函数间传递映射
映射是引用类型,因此在函数间传递映射时,传递的是映射的引用。这意味着,如果在函数内部修改了映射的内容,这些修改将会影响到原始的映射。
下面是一个示例,展示了如何在函数间传递映射,并在函数内部修改映射
package main
import "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 数据结构库,包含链表、栈、哈希表、树等。