原文链接:Golang - Implementing Heap Data Structure (and Heap Sort)
作者:Soham Kamani
堆数据结构 Heap
堆是一种基于树的数据结构,每个父节点与其子节点之间具有固定的关系。
堆通常用于对集合进行部分排序。集合中的每个插入/删除操作后都会进行"修复",以恢复最小堆或最大堆的完整性。
例如,最大堆可以表示为二叉树,其中每个父级都比其子级"更大"。通常需要少量的交换来"修复"一棵树,以在插入或删除后恢复最大堆属性。即使所有集合元素都不是全局排序的,"最大"值始终位于最大堆的顶部。对于最大堆,这意味着任何父节点的值必须大于或等于其子节点。这意味着根节点将始终具有最大值。
最小堆具有相反的属性,因此最小堆的根节点将始终具有最小值。
因此,堆具有多种实际应用。Go 语言中的 heap
包为任何实现 heap.Interface
接口的类型提供堆操作。堆是一棵树,其属性是每个节点都是其子树中的最小值节点。树中的最小元素是根,索引为 0。
container/heap
堆可以实现为带有节点和指针的二叉树,但大多数编程语言都为列表提供默认的堆实现。在 Python 中我们使用习惯的用法:
python
h = []
>>> heappush(h, (5, 'write code'))
>>> heappush(h, (7, 'release product'))
>>> heappush(h, (1, 'write spec'))
>>> heappush(h, (3, 'create tests'))
>>> heappop(h)
(1, 'write spec')
为了进行比较,下面是改编自 container/heap
文档的 Go 中的等效代码:
go
import (
"container/heap"
"fmt"
)
type Tuple struct {
i int
s string
}
type TupleHeap []Tuple
func (h TupleHeap) Len() int { return len(h) }
func (h TupleHeap) Less(i, j int) bool {
if h[i].i != h[j].i {
return h[i].i < h[j].i
}
return h[i].s < h[j].s
}
func (h TupleHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *TupleHeap) Push(x any) {
// Push 和 Pop 使用指针接收器,因为它们会修改切片的长度,而不只是内容
*h = append(*h, x.(Tuple))
}
func (h *TupleHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
// This example inserts several ints into an TupleHeap, checks
// the minimum, and removes them in order of priority.
func main() {
h := &TupleHeap{}
heap.Init(h)
heap.Push(h, Tuple{i: 5, s: "write code"})
heap.Push(h, Tuple{i: 7, s: "release product"})
heap.Push(h, Tuple{i: 1, s: "write spec"})
heap.Push(h, Tuple{i: 3, s: "create tests"})
fmt.Printf("%d ", heap.Pop(h))
// main.Tuple{i:1, s:"write spec"}
// Program exited.
}
collections/heap
的突出之处在于它具有广泛的通用性,但需要大量模板文件。 heap.Interface
支持二进制堆、数组堆、磁盘支持的堆以及任何用户支持的类型。
Golang 标准库的大部分目标是最大程度地简单,因此这种设计特性并不令人意外。
为啥需要使用到堆结构
通常,我们通过 "heapifying "一组值来使用堆,也就是将元素列表转换成堆:
堆的一些关键特性使其非常有用:
- 我们可以在
O(1)
的时间内找到一组 "n" 值的最大值或最小值。如果我们使用基于数组的列表,通常需要O(n)
的时间。 - 添加和删除值需要
O(log(n))
时间,同时保留最大或最小堆属性。在使用堆排序法对项目列表进行排序时,每个元素都会逐个插入堆中。
Go 实现堆数据结构和堆排序
我们可以通过实现常规列表的堆接口,将其转换为堆。
堆接口有五个方法,一旦我们实现了这些方法,就可以使用 container/heap
库提供的函数。
例如,让我们看看如何为整数列表实现堆接口:
- 我们需要定义一个自定义类型,而不是使用原始整数切片,因为我们需要在该类型上定义实现堆接口的方法:
go
type intHeap []int
Len()
是集合中元素的数量:
go
func (h intHeap) Len() int {
return len(h)
}
Less()
判断索引为i
的元素是否必须排序在索引为j
的元素之前。这将决定堆是最小堆还是最大堆
go
func (h intHeap) Less(i int, j int) bool {
return h[i] < h[j]
}
Swap()
交换索引为i
和j
的元素。
go
func (h intHeap) Swap(i int, j int) {
h[i], h[j] = h[j], h[i]
}
Push()
和Pop()
用于追加和删除片段的最后一个元素
go
func (h *intHeap) Push(x any) {
*h = append(*h, x.(int))
}
func (h *intHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
现在,我们 intHeap
类型实现的堆接口 heap.Interface
,我们可以使用该功能提供的 container/heap
包,以执行各种操作。
- 进入我们的
main
函数
go
package main
import (
"container/heap"
"fmt"
)
func main() {
// 创建 intHeap 实例
nums := &intHeap{3, 1, 4, 5, 1, 1, 2, 6}
// `Init` 函数将数字重新排列到一个堆中
heap.Init(nums)
// 切片现在重新排序,以符合堆属性
fmt.Println(nums)
}
输出:
css
&[1 1 1 5 3 4 2 6]
请注意,这些数字并不一定按顺序排列。相反,它们是以堆数据结构的形式排列的,如下所示:
从堆中添加和删除元素
我们可以使用 heap.Pop
函数从堆中获取当前最小(或最大)元素。该元素也会从堆中移除,堆将被重组以保持堆属性。
例如,如果我们想从 nums 中移除最小元素:
go
min := heap.Pop(nums)
fmt.Println("min: ", min, " heap: ", *nums)
输出:
arduino
min: 1 heap: [1 3 1 5 6 4 2]
要向堆中添加元素,我们可以使用 heap.Push
函数,同时输入要添加元素的值:
go
heap.Push(nums, 5)
fmt.Println("heap: ", *nums)
// 输出:heap: [1 3 1 5 6 4 2 5]
需要注意的是,添加和删除元素都需要 O(log(n))
时间,因此使用堆是维护一个可高效添加和删除元素的优先队列的好方法。
堆排序
由于 pop
函数总是返回最小的元素,因此我们可以连续调用堆中所有元素的 pop
函数,从而得到一个已排序的数字列表:
go
sortedNums := []int{}
for nums.Len() > 0 {
sortedNums = append(sortedNums, heap.Pop(nums).(int))
}
fmt.Println("sorted values: ", sortedNums)
输出:
ini
sorted values: [1 1 2 3 4 5 5 6]
排序操作必须针对所有项目 pop 每个数据,因此总体时间复杂度为 O(n*log(n))
。
使用自定义结构类型创建堆
到目前为止,我们使用的是 int
堆,但只要我们知道所需的排序属性,就可以用任何类型的列表创建堆。 例如,考虑一个包含字符串字段和日期字段的结构体列表:
go
type Holiday struct {
name string
date time.Time
}
// 为了方便起见,我们可以创建一个 String 来打印 Holiday 实例
func (h Holiday) String() string {
return "[" + h.name + ", " + h.date.Format("Jan 2") + "]"
}
// 我们可以创建一个新类型来表示假期列表
type Holidays []Holiday
现在,我们可以通过两种方式对假日实例列表进行排序:
- 按姓名字母顺序排列
- 按日历日期
首先,让我们实现假日类型的堆接口,以便按日历日期排序:
go
func (h Holidays) Less(i, j int) bool {
return h[i].date.Before(h[j].date)
}
func (h Holidays) Len() int { return len(h) }
func (h Holidays) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *Holidays) Push(x interface{}) {
*h = append(*h, x.(Holiday))
}
func (h *Holidays) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
现在,我们可以使用堆方法来创建我们的 Holidays 实例:
go
holidays := &Holidays{}
heap.Init(holidays)
heap.Push(holidays, Holiday{name: "Christmas", date: time.Date(2023, time.December, 25, 0, 0, 0, 0, time.Local)})
heap.Push(holidays, Holiday{name: "Labour Day", date: time.Date(2023, time.May, 1, 0, 0, 0, 0, time.Local)})
heap.Push(holidays, Holiday{name: "Diwali", date: time.Date(2023, time.November, 23, 0, 0, 0, 0, time.Local)})
fmt.Println("holidays: ", holidays)
输出:
lua
holidays: &[[Labour Day, May 1] [Christmas, Dec 25] [Diwali, Nov 23]]
如果我们现在想按字母顺序排列假期,就需要更改 Less 方法,使用名称字段进行比较:
go
func (h Holidays) Less(i, j int) bool {
return strings.Compare(h[i].name, h[j].name) < 0
}
如果我们运行相同的堆示例,我们的新输出将是:
go
holidays: &[[Christmas, Dec 25] [Labour Day, May 1] [Diwali, Nov 23]]
如果您想查看本文中所有示例的代码,可以在 Github 上查看或在 Go Playground 上运行实时示例