likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 5

原文链接:Golang - Implementing Heap Data Structure (and Heap Sort)

作者:Soham Kamani

堆数据结构 Heap

堆是一种基于树的数据结构,每个父节点与其子节点之间具有固定的关系。

堆通常用于对集合进行部分排序。集合中的每个插入/删除操作后都会进行“修复”,以恢复最小堆或最大堆的完整性。

例如,最大堆可以表示为二叉树,其中每个父级都比其子级“更大”。通常需要少量的交换来“修复”一棵树,以在插入或删除后恢复最大堆属性。即使所有集合元素都不是全局排序的,“最大”值始终位于最大堆的顶部。对于最大堆,这意味着任何父节点的值必须大于或等于其子节点。这意味着根节点将始终具有最大值。

最小堆具有相反的属性,因此最小堆的根节点将始终具有最小值。

因此,堆具有多种实际应用。Go 语言中的 heap 包为任何实现 heap.Interface 接口的类型提供堆操作。堆是一棵树,其属性是每个节点都是其子树中的最小值节点。树中的最小元素是根,索引为 0。

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

container/heap

堆可以实现为带有节点和指针的二叉树,但大多数编程语言都为列表提供默认的堆实现。在 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 中的等效代码:

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 "一组值来使用堆,也就是将元素列表转换成堆:

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

堆的一些关键特性使其非常有用:

  • 我们可以在 O(1) 的时间内找到一组 "n" 值的最大值或最小值。如果我们使用基于数组的列表,通常需要 O(n) 的时间。
  • 添加和删除值需要 O(log(n)) 时间,同时保留最大或最小堆属性。在使用堆排序法对项目列表进行排序时,每个元素都会逐个插入堆中。

Go 实现堆数据结构和堆排序

我们可以通过实现常规列表的堆接口,将其转换为堆。

堆接口有五个方法,一旦我们实现了这些方法,就可以使用 container/heap 库提供的函数。

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

  1. 我们需要定义一个自定义类型,而不是使用原始整数切片,因为我们需要在该类型上定义实现堆接口的方法:
type intHeap []int
  1. Len() 是集合中元素的数量:
func (h intHeap) Len() int {
	return len(h)
}
  1. Less() 判断索引为 i 的元素是否必须排序在索引为 j 的元素之前。这将决定堆是最小堆还是最大堆
func (h intHeap) Less(i int, j int) bool {
	return h[i] < h[j]
}
  1. Swap() 交换索引为 ij 的元素。
func (h intHeap) Swap(i int, j int) {
	h[i], h[j] = h[j], h[i]
}
  1. Push()Pop() 用于追加和删除片段的最后一个元素
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 函数
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)
}

输出:

&[1 1 1 5 3 4 2 6]

请注意,这些数字并不一定按顺序排列。相反,它们是以堆数据结构的形式排列的,如下所示:

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

从堆中添加和删除元素

我们可以使用 heap.Pop 函数从堆中获取当前最小(或最大)元素。该元素也会从堆中移除,堆将被重组以保持堆属性。

例如,如果我们想从 nums 中移除最小元素:

min := heap.Pop(nums)
fmt.Println("min: ", min, " heap: ", *nums)

输出:

min:  1  heap:  [1 3 1 5 6 4 2]

要向堆中添加元素,我们可以使用 heap.Push 函数,同时输入要添加元素的值:

heap.Push(nums, 5)
fmt.Println("heap: ", *nums)

// 输出:heap:  [1 3 1 5 6 4 2 5]

需要注意的是,添加和删除元素都需要 O(log(n)) 时间,因此使用堆是维护一个可高效添加和删除元素的优先队列的好方法。

堆排序

由于 pop 函数总是返回最小的元素,因此我们可以连续调用堆中所有元素的 pop 函数,从而得到一个已排序的数字列表:

sortedNums := []int{}
for nums.Len() > 0 {
  sortedNums = append(sortedNums, heap.Pop(nums).(int))
}
fmt.Println("sorted values: ", sortedNums)

输出:

sorted values:  [1 1 2 3 4 5 5 6]

排序操作必须针对所有项目 pop 每个数据,因此总体时间复杂度为 O(n*log(n))

使用自定义结构类型创建堆

到目前为止,我们使用的是 int 堆,但只要我们知道所需的排序属性,就可以用任何类型的列表创建堆。 例如,考虑一个包含字符串字段和日期字段的结构体列表:

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. 按日历日期

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

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 实例:

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)

输出:

holidays:  &[[Labour Day, May 1] [Christmas, Dec 25] [Diwali, Nov 23]]

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

如果我们现在想按字母顺序排列假期,就需要更改 Less 方法,使用名称字段进行比较:

func (h Holidays) Less(i, j int) bool {
	return strings.Compare(h[i].name, h[j].name) < 0
}

如果我们运行相同的堆示例,我们的新输出将是:

holidays:  &[[Christmas, Dec 25] [Labour Day, May 1] [Diwali, Nov 23]]

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

如果您想查看本文中所有示例的代码,可以在 Github 上查看或在 Go Playground 上运行实时示例