当前位置: 首页 > article >正文

Go基本数据结构

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 = 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 数据结构库,包含链表、栈、哈希表、树等。

http://www.kler.cn/news/337581.html

相关文章:

  • C++ 3D冒险游戏开发案例
  • 如何让算法拥有“记忆”?一文读懂记忆化搜索
  • 【重学 MySQL】五十二、MySQL8 新特性:计算列
  • 成都百洲文化传媒有限公司怎么样可靠不?
  • 机器学习——强化学习与深度强化学习
  • 【客户服务】万亿级智能客服系统架构设计与落地
  • 基于VITA57.1标准的8路SFP+光纤通道数据传输FMC子卡模块
  • 【计算机网络 - 基础问题】每日 3 题(二十九)
  • STM32CUBEIDE FreeRTOS操作教程(六):recursive mutexes递归互斥信号量
  • 【devops】devops-ansible之剧本初出茅庐--搭建rsync和nfs
  • 【JAVA基础】集合类之HashSet的原理及应用
  • Charles安卓抓包环境配置
  • ChatGPT背景下,高职人工智能技术应用专业的人才培养
  • 约数个数约数之和
  • 数字图像处理:边缘检测
  • SpringBoot中使用Redis实现排行榜功能,并考虑到 当用户积分相同时,要求按最后更新时间升序
  • 【移动端】Viewport 视口
  • 图解Linux文件属性与目录配置
  • canvas:绘制点和点之间连线
  • HCIP--以太网交换安全(二)