Golang源码学习之heap

最近刷题遇到了「堆」相关的使用,正好学习一下在Golang中heap的使用和实现。

heap包使用

Golang标准库中实现了heap包,位于container/heap。堆需要支持的操作抽象定义在heap.Interface接口中,自定义的数据结构实现该接口就可以实现堆了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// An IntHeap is a min-heap of ints.
type IntHeap []int

// sort.Interface接口实现
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } // 升序,小顶堆
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }

func (h *IntHeap) Push(x any) {
// Push and Pop use pointer receivers because they modify the slice's length,
// not just its contents.
*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
}

使用示例

heap.Interface 接口

要维持堆的性质数据结构必需可排序,因此嵌入了sort.Interface接口。
PushPop对应入堆和出堆操作中增减元素实现,堆调整的逻辑由标准库实现。

1
2
3
4
5
type Interface interface {
sort.Interface
Push(x any) // add x as element Len()
Pop() any // remove and return element Len() - 1.
}

其他函数

1
2
3
4
5
6
7
8
9
10
// 初始化建堆
func Init(h Interface)
// 弹出堆顶元素
func Pop(h Interface) any
// 入堆
func Push(h Interface, x any)
// 移除位置i的元素
func Remove(h Interface, i int) any
// 修改i位置元素值之后,调整堆
func Fix(h Interface, i int)

heap包实现

三个基本操作Init,Push,Pop的实现调用了up/down方法来调整堆元素顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func Init(h Interface) {
// heapify
n := h.Len()
for i := n/2 - 1; i >= 0; i-- {
down(h, i, n)
}
}

// 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)
}

// 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) // 遵循接口Pop() any方法调用约定,把堆顶元素放到n的位置
down(h, 0, n)
return h.Pop()
}

up方法,用于push之后从堆底向上调整

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
}
}

down方法,用于初始化或者pop从堆上层向下调整

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
}

计算左右孩子节点公式,注意右孩子j2有可能会越界

1
2
3
4
5
6
7
当前索引位置 i
左孩子 j1 = 2*i + 1
右孩子 j2 = 2*i + 2 = j1 + 1

例子:
1. 长度为偶数的堆 [100,200],索引位置0->[1,2],右孩子索引2越界
2. 长度为奇数的堆 [100,200,300],索引位置0->[1,2]

堆元素顺序判断条件!h.Less(j, i)看起来有点难理解,其实是为了Less方法实现在直觉上跟sort包排序保持一致,比如

1
2
3
4
5
1. 升序 => 小顶堆
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }

2. 降序 => 大顶堆
func (h IntHeap) Less(i, j int) bool { return h[i] > h[j] }
打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2015-2024 RivenZoo
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信