【译】Golang 实现堆数据结构与堆排序

原文链接: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 库提供的函数。

例如,让我们看看如何为整数列表实现堆接口:

  1. 我们需要定义一个自定义类型,而不是使用原始整数切片,因为我们需要在该类型上定义实现堆接口的方法:
go 复制代码
type intHeap []int
  1. Len() 是集合中元素的数量:
go 复制代码
func (h intHeap) Len() int {
	return len(h)
}
  1. Less() 判断索引为 i 的元素是否必须排序在索引为 j 的元素之前。这将决定堆是最小堆还是最大堆
go 复制代码
func (h intHeap) Less(i int, j int) bool {
	return h[i] < h[j]
}
  1. Swap() 交换索引为 ij 的元素。
go 复制代码
func (h intHeap) Swap(i int, j int) {
	h[i], h[j] = h[j], h[i]
}
  1. 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 包,以执行各种操作。

  1. 进入我们的 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

现在,我们可以通过两种方式对假日实例列表进行排序:

  1. 按姓名字母顺序排列
  2. 按日历日期

首先,让我们实现假日类型的堆接口,以便按日历日期排序:

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 上运行实时示例

相关推荐
一弓虽9 分钟前
SpringBoot 学习
java·spring boot·后端·学习
姑苏洛言18 分钟前
扫码小程序实现仓库进销存管理中遇到的问题 setStorageSync 存储大小限制错误解决方案
前端·后端
光而不耀@lgy33 分钟前
C++初登门槛
linux·开发语言·网络·c++·后端
方圆想当图灵1 小时前
由 Mybatis 源码畅谈软件设计(七):SQL “染色” 拦截器实战
后端·mybatis·代码规范
毅航1 小时前
MyBatis 事务管理:一文掌握Mybatis事务管理核心逻辑
java·后端·mybatis
我的golang之路果然有问题2 小时前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
柏油2 小时前
MySql InnoDB 事务实现之 undo log 日志
数据库·后端·mysql
写bug写bug3 小时前
Java Streams 中的7个常见错误
java·后端
Luck小吕4 小时前
两天两夜!这个 GB28181 的坑让我差点卸载 VSCode
后端·网络协议
M1A14 小时前
全栈开发必备:Windows安装VS Code全流程
前端·后端·全栈