Golang中的heap

golang中的heap

本文代码基于golang 1.24进行讲解(水平有限,共同探讨,不足之处欢迎指正)

小根堆(Min Heap)是一种特殊的完全二叉树数据结构,其根节点始终是堆中的最小元素,每个节点的值都小于或等于其子节点的值。在 Go 语言中实现小根堆 / 大根堆,可以通过标准库container/heap接口实现。

主要的实现思路其实就是用以为数组来进行模拟堆,具体如下图:

上图是以索引1为根节点的小根堆

上图是以索引0为根节点的小根堆

在上图结构的基础上,还需要还需要实现两个最重要的函数:

  • Down(…):负责将元素从当前位置向下调整至子节点合适的位置
  • Up(…):负责将元素从当前位置向上调整至父节点合适的位置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// heap
type Interface interface {
	sort.Interface
	Push(x any) // add x as element Len()
	Pop() any   // remove and return element Len() - 1.
}

// sort
type Interface interface {
	// Len is the number of elements in the collection.
	Len() int

	// Less reports whether the element with index i
	// must sort before the element with index j.
	//
	// If both Less(i, j) and Less(j, i) are false,
	// then the elements at index i and j are considered equal.
	// Sort may place equal elements in any order in the final result,
	// while Stable preserves the original input order of equal elements.
	//
	// Less must describe a transitive ordering:
	//  - if both Less(i, j) and Less(j, k) are true, then Less(i, k) must be true as well.
	//  - if both Less(i, j) and Less(j, k) are false, then Less(i, k) must be false as well.
	//
	// Note that floating-point comparison (the < operator on float32 or float64 values)
	// is not a transitive ordering when not-a-number (NaN) values are involved.
	// See Float64Slice.Less for a correct implementation for floating-point values.
	Less(i, j int) bool

	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

创建heap需要实现以下这几个函数,这些函数仅操作模拟的数组,不涉及堆操作:

  • Len():获取数组长度
  • Less(i, j int):返回第i个元素 < 第j个元素或者第i个元素 > 第j个元素(小根堆和大根堆就是根据这个区别的)
  • Swap(i, j int):交换第i个元素和第j个元素
  • Push(…):向数组插入一个元素
  • Pop():弹出数组中最后一个元素
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Init establishes the heap invariants required by the other routines in this package.
// Init is idempotent with respect to the heap invariants
// and may be called whenever the heap invariants may have been invalidated.
// The complexity is O(n) where n = h.Len().
func Init(h Interface) {
	// heapify
	n := h.Len()
	for i := n/2 - 1; i >= 0; i-- {
		down(h, i, n)
	}
}

根据索引0为根节点初始化的堆,具体流程如下:

  1. 获取数组长度
  2. 从大到小遍历非叶子节点,进行down操作,向下层进行调整
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func down(h Interface, i0, n int) bool {
	i := i0
	for {
		j1 := 2*i + 1
		if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
			break
		}
		j := j1 // left child
		if j2 := j1 + 1; j2 < n && h.Less(j2, j1) {
			j = j2 // = 2*i + 2  // right child
		}
		if !h.Less(j, i) {
			break
		}
		h.Swap(i, j)
		i = j
	}
	return i > i0
}

down操作,将当前元素向下调整,具体流程如下:

  1. 记录当前父节点
  2. 判断父节点的左儿子是否存在(是否超出数组长度 || 索引精度是否溢出)
  3. 判断父节点的右儿子是否存在,并且判断右儿子是否小于左儿子(小根堆的思路)
  4. 判断左右儿子中最小的值,是否比父节点还小(小根堆逻辑)
  5. 交换成父节点变为三个节点中的最小值,符合小根堆定义。
  6. 继续处理被交换后的儿子节点(down向下处理,递归思路)
  7. 返回是否有交换发生(小根堆结构维护)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func up(h Interface, j int) {
	for {
		i := (j - 1) / 2 // parent
		if i == j || !h.Less(j, i) {
			break
		}
		h.Swap(i, j)
		j = i
	}
}

up操作,将当前元素向上调整,具体流程如下:

  1. 获取当前节点的父节点
  2. 判断当前节点是否是根节点 || 判断当前节点是否小于父节点(小根堆的思路)
  3. 交换当前节点和父节点的值,使父节点变为两个节点中的最小值,符合小根堆定义。
  4. 继续处理被交换后的父节点(up向上处理,递归思路)
1
2
3
4
5
6
// Push pushes the element x onto the heap.
// The complexity is O(log n) where n = h.Len().
func Push(h Interface, x any) {
	h.Push(x)
	up(h, h.Len()-1)
}

Push函数具,在堆中插入一个元素,具体流程如下:

  1. 调用数组定义的Push()函数,将元素插入数组尾部
  2. 将该元素向上调整

最后一个元素在堆中默认定义就是最大的元素,所以插入后只需要对其进行up操作,就可以完成小根堆结构维护。

1
2
3
4
5
6
7
8
9
// Pop removes and returns the minimum element (according to Less) from the heap.
// The complexity is O(log n) where n = h.Len().
// Pop is equivalent to [Remove](h, 0).
func Pop(h Interface) any {
	n := h.Len() - 1
	h.Swap(0, n)
	down(h, 0, n)
	return h.Pop()
}

Pop函数,弹出堆中最小的元素,具体流程如下:

  1. 调用数组定义的Len()函数,获取最后一个元素的索引
  2. 调用数组定义的Swap()函数,交换第一个元素和最后一个元素
  3. 排除最后一个元素,进行down操作(Initi()的时候,使用的是h.Len(),这里用的是h.Len() - 1)
  4. 调用数组定义的Pop()函数,删除最后一个元素

经常鲨人的朋友应该都知道,在排成一队往前看的人中,去鲨第一个人是很难的事情,一动手后面的人就都看到了,但是对队尾的人出手,前面的人根本不会知道,可以做到神不知鬼不觉。同理,我们只需要将队头和队尾进行交换,再排除队尾,将队头进行小根堆结构维护,队尾的元素就是要弹出堆的最小值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Remove removes and returns the element at index i from the heap.
// The complexity is O(log n) where n = h.Len().
func Remove(h Interface, i int) any {
	n := h.Len() - 1
	if n != i {
		h.Swap(i, n)
		if !down(h, i, n) {
			up(h, i)
		}
	}
	return h.Pop()
}

Remove函数,弹出堆中的任一元素,具体流程如下:

  1. 调用数组定义的Len()函数,获取最后一个元素的索引
  2. 判断要弹出的元素是否是最后一个元素
  3. 调用数组定义的Swap()函数,交换当前元素和最后一个元素,由于不确定对于小根堆结构的关系是变大还是变小,所以会在排除最后一个元素,进行一次down操作,如果down操作未发生结构维护,则再进行一次up操作
  4. 调用数组定义的Pop()函数,删除最后一个元素

和Pop()相同的原理.

1
2
3
4
5
6
7
8
9
// Fix re-establishes the heap ordering after the element at index i has changed its value.
// Changing the value of the element at index i and then calling Fix is equivalent to,
// but less expensive than, calling [Remove](h, i) followed by a Push of the new value.
// The complexity is O(log n) where n = h.Len().
func Fix(h Interface, i int) {
	if !down(h, i, h.Len()) {
		up(h, i)
	}
}

Fix函数,在修改了堆中某个元素的值之后,重新恢复堆的顺序特性,具体流程如下:

1.由于不确定该元素的修改,对于小根堆结构的关系是变大还是变小,所以会先进行一次down操作,如果down操作未发生结构维护,则再进行一次up操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
	"container/heap"
	"fmt"
)

// MinHeap 定义小根堆类型
type MinHeap []int

// Len 实现 heap.Interface 接口的 Len 方法
func (h MinHeap) Len() int { return len(h) }

// Less 实现 heap.Interface 接口的 Less 方法(小根堆逻辑,大根堆使用 > )
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }

// Swap 实现 heap.Interface 接口的 Swap 方法
func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }

// Push 实现 heap.Interface 接口的 Push 方法
func (h *MinHeap) Push(x any) {
	*h = append(*h, x.(int))
}

// Pop 实现 heap.Interface 接口的 Pop 方法
func (h *MinHeap) Pop() any {
	n := len(*h)
	x := (*h)[n-1]
	*h = (*h)[0 : n-1]
	return x
}

func main() {
	// 创建并初始化小根堆
	minHeap := &MinHeap{5, 2, 6, 3, 1, 4}

	heap.Init(minHeap)

	// 添加新元素
	heap.Push(minHeap, 0)
	heap.Push(minHeap, 7)

	// 弹出并打印最小值
	fmt.Println("Min element:", heap.Pop(minHeap)) // 0

	// 查看当前最小值(不弹出)
	fmt.Println("Current min:", (*minHeap)[0]) // 1

	// 遍历弹出所有元素
	fmt.Println("All elements in ascending order:")
	for minHeap.Len() > 0 {
		fmt.Print(heap.Pop(minHeap), " ") // 1, 2, 3, 4, 5, 6, 7
	}
}
  • container/heap包主要的作用是维护堆的数据结构,涉及到对于堆内数据的操作,都是调用底层一维数组实现的函数进行操作的
  • 通过改变底层一维数组定义的Less()函数,使用><来决定是小根堆或是大根堆