堆的Go语言实现
洪笳淏 Lv4

堆的简介

  首先,堆和二叉树有啥关系呢,为什么人们总数把堆画成一棵二叉树?因为,堆在逻辑上其实是一种特殊的二叉树(完全二叉树),只不过存储在数组里。一般的链表二叉树,我们操作节点的指针,而在数组里,我们把数组索引作为指针:

  画个图你立即就能理解了,比如 arr 是一个字符数组,注意数组的第一个索引 0 空着不用:

avatar

  因为这棵二叉树是「完全二叉树」,所以把 arr[1] 作为整棵树的根的话,每个节点的父节点和左右孩子的索引都可以通过简单的运算得到,这就是二叉堆设计的一个巧妙之处。

  为了方便讲解,下面都会画的图都是二叉树结构,相信你能把树和数组对应起来。二叉堆还分为最大堆和最小堆。最大堆的性质是:每个节点都大于等于它的两个子节点。类似的,最小堆的性质是:每个节点都小于等于它的子节点。

堆的实现

  在Go语言的标准库container/heap中,实现了二叉堆,我们先来看看这个包下面有什么内容。

  1. 接口定义
1
2
3
4
5
type Interface interface {
sort.Interface
Push(x interface{})
Pop() interface{}
}

可以看出,这个堆结构继承自 sort.Interface, 回顾下 sort.Interface,它需要实现三个方法

1
2
3
4
5
6
7
type Interface interface {
// Len is the number of elements in the collection.
Len() int
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}

加上堆接口定义的两个方法

1
2
func Push(h Interface, x interface{}) {}
func Pop(h Interface) interface{} {}

  也就是说,要实现一个堆需要完整实现上述5个方法。

  1. 上浮up和下沉down方法

  我们要讲的是最小堆,每个节点都比它的两个子节点小,但是在插入元素和删除元素时,难免破坏堆的性质,这就需要通过这两个操作来恢复堆的性质了。

  对于最小堆,会破坏堆性质的有有两种情况:

  • down:如果某个节点 A 比它的子节点(中的一个)大,那么 A 就不配做父节点,应该下去,下面那个更小的节点上来做父节点,这就是对 A 进行下沉
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
}
  • up:如果某个节点 A 比它的父节点小,那么 A 不应该做子节点,应该把父节点换下来,自己去做父节点,这就是对 A 的上浮
1
2
3
4
5
6
7
8
9
10
11
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
}
}

  当然,错位的节点 A 可能要上浮(或下沉)很多次,才能到达正确的位置,恢复堆的性质。所以代码中肯定有一个 for 循环。

  • fix:当堆中某一节点的值发生改变后,要对其进行上浮下沉操作
1
2
3
4
5
func Fix(h Interface, i int) {
if !down(h, i, h.Len()) {
up(h, i)
}
}
  1. 初始化结构体(建堆)
1
2
3
4
5
6
7
func Init(h Interface) {
// heapify
n := h.Len()
for i := n/2 - 1; i >= 0; i-- {
down(h, i, n)
}
}

  可见,在Go语言中是以0为首开始存储的,所以对于这歌满二叉树,索引为x的节点左子树索引为2*x+1,右子树为2*x+2

  1. 堆的增,出堆和删操作
  • Push

  当在当前堆中增加元素时,先在队列的最末尾处(完全二叉树的最后一个节点)将其插入,然后再利用上浮操作调整其位置。

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 interface{}) {
h.Push(x) // h.Push(x)是外层实体的方法,与本方法不相同
up(h, h.Len()-1)
}
  • 出堆Pop

  堆的Pop操作是先将堆顶元素与最后一个元素交换位置,然后再将交换后的堆顶元素下沉到属于它的位置。

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) interface{} {
n := h.Len() - 1
h.Swap(0, n)
down(h, 0, n)
return h.Pop() // 这里的h.Pop()是用户自己定义的方法,以上部分操作都只负责将堆顶元素放到末尾并使堆保持其规则,最后这一行是用户自己对实体的操作
}
  • 删除任意节点Remove

  删除任意节点的索引为i,将其与最后一个节点调换位置,然后将调换到索引i处的节点下沉到属于它的位置。特别的:出堆操作就是删除最后一个节点。

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) interface{} {
n := h.Len() - 1
if n != i {
h.Swap(i, n)
if !down(h, i, n) {
up(h, i)
}
}
return h.Pop()
}

实现int类型的堆

  在官方文档给的示例中example_intheap_test.go,介绍了怎么通过调用heap包来实现int类型的二叉堆。我们来看看其具体的实现流程。

  1. 定义一个新的类型,实现五个固定的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// An IntHeap is a min-heap of ints.
type IntHeap []int

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 interface{}) { // 增添一个新元素
// 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() interface{} { // 推出最后一个元素
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
  1. 完整代码
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
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This example demonstrates an integer heap built using the heap interface.
package heap_test

import (
"container/heap"
"fmt"
)

// An IntHeap is a min-heap of ints.
type IntHeap []int

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 interface{}) {
// 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() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}

// This example inserts several ints into an IntHeap, checks the minimum,
// and removes them in order of priority.
func Example_intHeap() {
h := &IntHeap{2, 1, 5}
heap.Init(h) // 初始化这个堆,此时堆已经建好了
heap.Push(h, 3)
fmt.Printf("minimum: %d\n", (*h)[0])
for h.Len() > 0 {
fmt.Printf("%d ", heap.Pop(h))
}
// Output:
// minimum: 1
// 1 2 3 5
}

优先队列

  在/src/container/heap/example_pq_test.go文件中给出了使用堆实现一个优先队列的示例。不同于int类型的堆,可以直接根据元素的值的大小决定其优先级,优先队列中的每个元素需要通过一个priority变量设置各自的优先级。同时,还需要设置一个变量index来保持与队列中的索引号一致。

1
2
3
4
5
6
7
// An Item is something we manage in a priority queue.
type Item struct {
value string // The value of the item; arbitrary.
priority int // The priority of the item in the queue.
// The index is needed by update and is maintained by the heap.Interface methods.
index int // The index of the item in the heap.
}

  然后就可以初始化一个堆了,我们要为这个优先队列设立Len()Swap()Less()三个方法使其先变成一个sort.Interface类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// A PriorityQueue implements heap.Interface and holds Items.
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int {
return len(pq)
}

func (pq PriorityQueue) Swap(i,j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}

func (pq PriorityQueue) Less(i,j int) bool {
// We want Pop to give us the highest, not lowest, priority so we use greater than here.
return pq[i].priority > pq[j].priority
}

  然后就是经典的Push()Pop()方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (pq *PriorityQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*Item)
item.index = n
*pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil // avoid memory leak
item.index = -1 // for safety
*pq = old[:n-1]
return item
}

  当某一个节点的值或优先级发生改变时,再实现一个update方法来调整优先队列的顺序。

1
2
3
4
5
func (pq *PriorityQueue) update(item *Item, value, priority) {
item.value = value
item.priority = priority
heap.fix(pq, item.index)
}

  完整的示例:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package heap_test

import (
"container/heap"
"fmt"
)

// An Item is something we manage in a priority queue.
type Item struct {
value string // The value of the item; arbitrary.
priority int // The priority of the item in the queue.
// The index is needed by update and is maintained by the heap.Interface methods.
index int // The index of the item in the heap.
}

// A PriorityQueue implements heap.Interface and holds Items.
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int {
return len(pq)
}

func (pq PriorityQueue) Swap(i,j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}

func (pq PriorityQueue) Less(i,j int) bool {
// We want Pop to give us the highest, not lowest, priority so we use greater than here.
return pq[i].priority > pq[j].priority
}

func (pq *PriorityQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*Item)
item.index = n
*pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil // avoid memory leak
item.index = -1 // for safety
*pq = old[:n-1]
return item
}

func (pq *PriorityQueue) update(item *Item, value string, priority int) {
item.value = value
item.priority = priority
heap.fix(pq, item.index)
}


// This example creates a PriorityQueue with some items, adds and manipulates an item,
// and then removes the items in priority order.
func Example_priorityQueue() {
// Some items and their priorities.
items := map[string]int{
"banana": 3, "apple": 2, "pear": 4,
}

// Create a priority queue, put the items in it, and
// establish the priority queue (heap) invariants.
pq := make(PriorityQueue,len(items))
i := 0
for value, priority := range items {
pq[i] = &Items {
value : value,
priority : priority,
index : i,
}
i++
}
heap.Init(&pq)

// Insert a new item and then modify its priority.
item := &Item{
value: "orange",
priority: 1,
}
heap.Push(&pq, item)
pq.update(item, item.value, 5)

// Take the items out; they arrive in decreasing priority order.
for pq.Len() > 0 {
item := heap.Pop(&pq).(*Item)
fmt.Printf("%.2d:%s ", item.priority, item.value)
}
// Output:
// 05:orange 04:pear 03:banana 02:apple
}

LeetCode经典题目

实现一个优先队列

  优先队列中的每个元素都有各自的优先级,优先级最高的元素最先得到服务;优先级相同的元素按照其在优先队列中的顺序得到服务。 优先队列往往用堆来实现。我们在上一节实现了一个int类型的堆,它的排序规则是直接根据存入的数据的值来决定的。对于优先队列而言,我们存入的数据可能是一个切片的索引数组,其排序规则是该切片的值的大小,只不过体现在堆中,存入的是其索引。这样我们就可以通过其索引控制出入优先队列是时机,并保证能够适时地得到堆顶的值。如下图所示:

avatar

  接下来我们来看一道很经典的题。

239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

示例 2:

1
2
输入:nums = [1], k = 1
输出:[1]

示例 3:

1
2
输入:nums = [1,-1], k = 1
输出:[1,-1]

示例 4:

1
2
输入:nums = [9,11], k = 2
输出:[11]

示例 5:

1
2
输入:nums = [4,-2], k = 2
输出:[4]

提示:

  • 1 <= nums.length <= 105

  • -104 <= nums[i] <= 104

  • 1 <= k <= nums.length

解题思路:

  由于每次都要找到最大值,很自然的想到一种数据结构:优先队列(堆),其中的大根堆可以帮助我们实时维护一系列元素中的最大值。

  对于本题而言,初始时,我们使用数组 nums 的前 k 个元素的索引初始化大根堆,堆中索引的排序规则是数组 nums 的大小所决定。每当我们向右移动窗口时,我们就可以把一个新的元素的索引放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。我们在什么情况下出堆呢?每次有新元素入堆时,我们都要先判断一下位于堆顶的索引是否<=新入堆元素索引-k。如果条件成立,则执行出堆操作,直到堆顶元素的索引在滑动窗口的范围内。

  本题可以学习到的很重要的一个技巧就是出堆的操作可以不用着急,可以根据题目的实际需求,判断堆顶元素的索引是否符合当前选择范围再统一执行出堆操作,直到堆顶元素代表的索引在要求的范围内。

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
import "container/heap"

var a []int
type IntHeap []int
func (h IntHeap) Len() int {
return len(h)
}

func (h IntHeap) Swap(i,j int) {
h[i], h[j] = h[j], h[i]
}

func (h IntHeap) Less(i,j int) bool {
return a[h[i]] > a[h[j]] // 排序规则不是索引大小,而是数组元素的值的大小
}


func (h *IntHeap) Push(x interface{}) {
*h = append(*h, x.(int))
}

func (h *IntHeap) Pop() interface{} {
old := *h
v := old[len(old)-1]
h = old[:len(old)-1]
return v
}

func maxSlidingWindow(nums []int, k int) []int {
a = nums
var h IntHeap

for i:=0; i<k; i++ {
h = append(h,i)
}
heap.Init(&h) // 初始化堆

ret := make([]int,1)
ret[0] = a[h[0]]

for i:=k; i<len(nums); i++ {
heap.Push(&h, i.(int))

for h[0] <= i-k { // 判断堆顶索引对应的元素是否在滑动窗口中,不在则持续出堆
heap.Pop(&h)
}
ret = append(ret, a[h[0]]) // 堆顶元素在滑动窗口中,加入输出数组
}
return ret
}
  • Post title:堆的Go语言实现
  • Post author:洪笳淏
  • Create time:2021-09-27 15:07:00
  • Post link:https://jiahaohong1997.github.io/2021/09/27/堆的Go语言实现/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments