Go‘s 数据结构
一、链表
单链表:
1 | |
循环链表:
1 | |
初始化循环链表:
1 | |
数组实现链表:【静态链表】
1 | |
二、栈和队列
go语言中,并没有栈与队列相关的数据结构,但是我们可以借助切片来实现栈与队列的操作;接下来我们一起实现栈与队列基本操作。
栈基本操作:
1 | |
队列基本操作:
1 | |
三、字典
3.1 字典
Golang 语言中字典 map 的实现由哈希表实现,具体可参考标准库 runtime 下的 map.go 文件。
go map 和 c++ stl map 比对
Go 语言的map与 C++ STL 中的map(实际分为std::map和std::unordered_map)的时间复杂度差异,核心源于底层数据结构的不同。下面从实现原理出发,对比两者的核心操作复杂度。
一、底层实现差异
- Go 的
map:底层是哈希表(Hash Table),采用 “拉链法”(分离链表法)解决哈希冲突。具体来说,数据被分配到多个bucket中,每个bucket存储哈希值低位相同的键值对;若冲突严重,bucket会通过链表扩展(溢出桶)。 - C++ STL 的
map:分为两种:std::map:底层是红黑树(一种平衡二叉搜索树),键值对按键的大小有序存储。std::unordered_map:底层是哈希表,与 Go 的map实现逻辑类似(拉链法解决冲突),键值对无序存储。
二、核心操作时间复杂度对比
1、插入(Insert)
-
Go 的
map:平均时间复杂度 O(1),最坏情况 O(n)。
原理:通过哈希函数计算键的哈希值,定位到对应的
bucket,插入键值对。若bucket未满,直接插入;若哈希冲突导致bucket溢出,需遍历链表找空位(最坏情况链表长度为 n)。此外,当负载因子(元素数 / 桶数)过高时,会触发扩容(rehash):重新分配更大的桶数组,复制旧元素到新桶,此时单次操作复杂度为 O (n),但均摊到多次插入后,平均仍为 O (1)。
-
C++
std::map:时间复杂度 O(log n)(n 为元素个数)。
原理:红黑树的插入需要先查找插入位置(O (log n)),再插入节点并通过旋转保持平衡(最多 O (log n) 次旋转),整体复杂度由树的高度决定(红黑树高度为 O (log n))。
-
C++
std::unordered_map:平均时间复杂度 O(1),最坏情况 O(n),与 Go 的
map一致。原理:哈希表插入逻辑与 Go 类似,扩容时同样可能触发 O (n) 的复制操作,但均摊后为 O (1)。
2、删除(Delete)
-
Go 的
map:平均时间复杂度 O(1),最坏情况 O(n)。
原理:通过哈希定位
bucket,遍历链表找到键并标记删除(或直接移除),最坏情况需遍历整个冲突链表(长度 n)。 -
C++
std::map:时间复杂度 O(log n)。
原理:红黑树中先查找节点(O (log n)),删除后通过旋转保持平衡(O (log n) 次操作)。
-
C++
std::unordered_map:平均时间复杂度 O(1),最坏情况 O(n),与 Go 的
map一致。
3、查找(Search)
-
Go 的
map:平均时间复杂度 O(1),最坏情况 O(n)。
原理:通过哈希定位
bucket,遍历链表匹配键,最坏情况需遍历整个冲突链表。 -
C++
std::map:时间复杂度 O(log n)。
原理:红黑树的二分查找,时间与树高成正比。
-
C++
std::unordered_map:平均时间复杂度 O(1),最坏情况 O(n),与 Go 的
map一致。
4、遍历(Traverse)
-
Go 的
map:时间复杂度 O(n)。
原理:需遍历所有
bucket及其中的链表,访问每个元素一次(无论是否冲突,总元素数为 n)。 -
C++
std::map:时间复杂度 O(n)。
原理:红黑树的中序遍历,按键的顺序访问所有节点,每个节点访问一次。
-
C++
std::unordered_map:时间复杂度 O(n),与 Go 的
map一致(遍历所有桶和元素)。
三、总结
| 操作 | Go 的map(哈希表) |
C++ std::map(红黑树) |
C++ std::unordered_map(哈希表) |
|---|---|---|---|
| 插入 | 平均 O (1),最坏 O (n) | O(log n) | 平均 O (1),最坏 O (n) |
| 删除 | 平均 O (1),最坏 O (n) | O(log n) | 平均 O (1),最坏 O (n) |
| 查找 | 平均 O (1),最坏 O (n) | O(log n) | 平均 O (1),最坏 O (n) |
| 遍历 | O(n) | O(n) | O(n) |
关键结论
- Go 的
map与 C++ 的std::unordered_map是 “同类”(哈希表实现),核心操作的时间复杂度完全一致,适合对插入 / 删除 / 查找效率要求高的场景(平均 O (1))。 - C++ 的
std::map(红黑树)的优势是键值对有序,但操作复杂度为 O (log n),适合需要按键排序的场景(如范围查询)。
在算法题中,若需 “无序映射”,Go 的map和 C++ 的std::unordered_map是等价选择;若需 “有序映射”,则 C++ 需用std::map,而 Go 需额外维护排序结构(如切片 + 排序)。
3.2 集合
技巧:用字典达到集合的效果
时间复杂度类似C++ std::unordered_set
声明:
1 | |
添加元素:
1 | |
ps: struct{}{}不占空间,所以set只占用key和自身数据结构的内存
检查存在:
1 | |
删除:
1 | |
四、树
二叉树节点:
1 | |
五、通用方法
5.1 sort
Go 语言的 sort 包提供了用于排序切片和自定义数据结构的强大工具和接口。这个包提供了几种不同的排序方法,让你能够有效地排序内置数据类型(如切片)以及通过实现 sort.Interface 来排序自定义数据类型。
- 内置类型的排序 (但是没有现成的降序排序函数)
对于基本数据类型的切片,如 int、float64 和 string,Go 的 sort 包提供了直接的排序函数:
-
排序整数:使用
sort.Ints函数1
2a := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}
sort.Ints(a) -
排序浮点数:使用
sort.Float64s函数1
2f := []float64{3.1, 3.14, 2.71}
sort.Float64s(f) -
排序字符串:使用
sort.Strings函数1
2s := []string{"Go", "Bravo", "Gopher", "Alpha"}
sort.Strings(s)
这些函数都是对切片进行原地排序,即不创建切片的副本进行排序,而是直接在原切片上进行排序。
- 自定义排序
如果你需要对自定义的数据结构进行排序,可以通过实现 sort.Interface 接口来实现。这个接口定义了三个方法:
Len()返回集合中的元素个数Less(i, j int) bool报告索引i的元素是否应该比索引j的元素排在前面Swap(i, j int)交换索引i和j位置的元素
通过实现这三个方法,你可以定义自定义类型的排序行为。
1 | |
在这个例子中,Person 结构体按照 Age 字段进行排序。
更简单的办法 - sort.Slice
非常推荐这个做法,可以优先用这个就好了
- 核心语法:
sort.Slice(切片名, func(i, j int) bool { return 比较规则 }) - 排序规则:
- 升序:
切片名[i] < 切片名[j] - 降序:
切片名[i] > 切片名[j]
- 升序:
示例:
1 | |
benchmark - 对比两种方式的执行速度
1 | |
1 | |
看起来性能下不错,使用sort.slice分配内存多了一倍,因为要反射构造一个新的接口变量,后续基于这个接口变量来排序,但是可以接受。
- 性能和稳定性
- 性能:Go 的排序算法主要是快速排序,它的平均时间复杂度是 O(n log n)。在最坏的情况下,也就是当输入数组已经接近排序完成时,快速排序的性能会降低,但 Go 实现中通过引入其他算法(如堆排序)作为补充来优化这种情况。
- 稳定排序:如果你需要保持等值元素的相对顺序,可以使用
sort.Stable方法。这确保如果Less方法报告元素i和j相等(即!Less(i, j) && !Less(j, i)),它们的相对顺序不会改变。
1 | |
这提供了在复杂数据排序时更细粒度的控制,特别是当数据的次序有实际意义时。
5.2 堆(优先级队列)container/heap
在 Go 语言中,优先级队列(Priority Queue)通常通过最小堆或最大堆实现。Go 标准库的 container/heap 包提供了堆的基础操作,可以帮助我们构建自定义的优先级队列。
- 基本原理
在 Go 中,container/heap 包实现的是最小堆(Min-Heap)。这意味着堆顶(即堆中第一个元素)总是最小的元素。如果我们需要实现最大堆(Max-Heap),可以通过对元素的比较操作进行反转来实现。
- 使用
container/heap实现优先级队列
要使用 container/heap 实现优先级队列,需要实现 heap.Interface 接口。该接口要求以下方法:
- Len() int:返回堆中元素的数量。
- Less(i, j int) bool:决定索引
i的元素是否应排在索引j的元素之前。在最小堆中,Less(i, j)应该返回true表示i小于j。 - Swap(i, j int):交换索引
i和索引j的元素。 - Push(x interface{}):将元素
x推入堆。 - Pop() interface{}:从堆中移除并返回堆顶元素。
- 示例:最小堆优先级队列
以下是一个简单的示例,演示如何使用 container/heap 实现一个整数优先级队列(最小堆):
1 | |
heap.Push和heap.Pop都会自动重建堆,只需要记住:
- 这个接口的堆顶索引为0
- heap.Push:放到末尾,自动重建操作为,先调用接口方法type.Push(),然后当前末尾元素“上浮”
- heap.Pop:出堆顶,自动重建操作为,先首尾元素交换,再调用type.Pop()弹出末尾元素(就是交换前堆顶的值),然后当前堆顶元素“下沉”
- 所以需要注意实现type.Push()和type.Pop(),都是在末尾操作
5.3 双向链表 container/list
基本用法(核心 API 与操作)
container/list是双向链表,核心结构体是*list.List(链表对象)和*list.Element(节点对象),常用 API 如下:
- 初始化链表
1 | |
- 新增元素(头部 / 尾部 / 指定位置) O1
1 | |
- 删除元素 O1
1 | |
- 遍历链表 On
1 | |
- move相关操作 O1
1 | |
- 其他常用操作 O1
1 | |
5.4 数学计算
以下是 Go 语言 math 包中常用的函数的总结,包括说明、使用方法以及时间复杂度:
math.Abs(x float64) float64
说明: 计算并返回 x 的绝对值。
使用:
1 | |
时间复杂度: O(1) – 绝对值的计算是一个简单的单步操作。
math.Ceil(x float64) float64
说明: 向上取整,将 x 向上舍入到最接近的整数值。
使用:
1 | |
时间复杂度: O(1) – 向上取整也是一个简单的单步操作。
math.Floor(x float64) float64
说明: 向下取整,将 x 向下舍入到最接近的整数值。
使用:
1 | |
时间复杂度: O(1) – 向下取整也是一个单步操作。
math.Pow(x, y float64) float64
说明: 计算 x 的 y 次方。
使用:
1 | |
时间复杂度: O(log y) – 使用快速幂算法实现,次方计算的时间复杂度与 y 的大小有关,通常为 O(log y)。
math.Sqrt(x float64) float64
说明: 计算 x 的平方根。
使用:
1 | |
时间复杂度: O(1) – 由于平方根使用高效算法实现,在硬件和算法优化下通常为常数时间。
math.Max(x, y float64) float64
说明: 返回 x 和 y 中的最大值。
使用:
1 | |
时间复杂度: O(1) – 最大值的计算仅涉及一次比较操作,因此是常数时间。
1.21版本后,内置了**max()**,可以对多个相同的数值类型,取最大值,推荐使用
math.Min(x, y float64) float64
说明: 返回 x 和 y 中的最小值。
使用:
1 | |
时间复杂度: O(1) – 最小值的计算仅涉及一次比较操作,因此是常数时间。
1.21版本后,内置了**min()**,可以对多个相同的数值类型,取最小值,推荐使用
- 最大值和最小值(浮点的和整形的)
math.MaxInt64:int64 类型的最大值(常量,9223372036854775807)。
math.MinInt64:int64 类型的最小值(常量,-9223372036854775808)。
math.MaxInt32:int32 类型的最大值(常量,2147483647)。
math.MinInt32:int32 类型的最小值(常量,-2147483648)。
math.MaxInt:当前系统中 int 类型的最大值(取决于系统位数,32 位系统为 2147483647,64 位系统为 9223372036854775807)。
math.MinInt:当前系统中 int 类型的最小值(与 MaxInt 对应,32 位为 -2147483648,64 位为 -9223372036854775808)。
math.MaxFloat64:float64 类型能表示的最大正值,约为 1.7976931348623157e+308。
math.MinFloat64:float64 类型能表示的最小正值(大于 0 的最小数),约为 2.2250738585072014e-308。
math.MaxFloat32:float32 类型能表示的最大正值,约为 3.4028234663852886e+38。
math.MinFloat32:float32 类型能表示的最小正值(大于 0 的最小数),约为 1.401298464324817e-45。
5.5 go浮点数判等
六、排序算法(例)
- 快排
查看输出
1 | |
输出为:
1 | |
易错点
- 列表传参数使用append函数
查看案例
列表是一个指针:
1 | |
但是下面逻辑出错:
1 | |
在 Go 中,slice 是通过引用传递的,但是对于 append 函数的具体行为,需要特别注意:
- 当
append导致 slice 容量增加时,会创建一个新的底层数组,原始的 slice 引用的数组不会被改变。 - 当
append操作未超过原 slice 的容量时,它会在原数组的基础上修改数据,并影响所有引用该数组的 slice。 - 在
inorder中对ans使用了append,这可能导致ans的底层数组发生变化(扩容操作),尤其是在多次append后超过了原始容量。
- 当
append导致扩容时,它返回的是一个指向新底层数组的新 slice。这意味着,虽然ans在inorder函数中被修改,但是这些修改不会反映到inorderTraversal函数中的ans上,因为它们可能指向不同的底层数组。
- 代码块问题 (变量遮蔽)
查看案例


这种内部范围的变量覆盖了返回值变量
修改方法:
1.内部范围变量重命名
2.return err

others
max min函数 https://blog.csdn.net/caspar_notes/article/details/132843940