0. 堆的定义

以下引自《算法导论》:

堆(Heap)是一个可以被看成近似完全二叉树的数组。树上的每一个结点对应数组的一个元素。除了最底层外,该树是完全充满的,而且是从左到右填充。堆包括最大堆和最小堆:最大堆的每一个节点(除了根结点)的值不大于其父节点;最小堆的每一个节点(除了根结点)的值不小于其父节点。

实现堆的方式有很多,最简单的就是采用完全二叉树来实现,我们称其为二叉堆,这种方式实现简单,但是效率一般。更高效的实现有斐波那契堆等,各种不同的实现比较请参考维基百科

本文讲解如何通过完全二叉树来实现一个大顶堆。以大顶堆为例,其满足下列性质:

  1. 它是一棵完全二叉树
  2. 树中的任意非空节点的值总是大于等于其孩子节点的值

大顶堆示意图:

heap-1

1. 常见操作

1.1. 外部操作

  • peek:取出堆顶元素
  • push:向堆中插入一个元素
  • pop:返回堆顶元素,并将堆顶元素删除
  • remove(i):删除并返回堆中指定下标为 i 的元素
  • size:获取堆中元素个数
  • isEmpty:判断堆是否为空
  • init:由一个普通数组初始化成一个堆结构性质的数组

1.2. 内部操作

  • heapifyUp:由下至上调整
  • heapifyDown:由上至下调整

2. 实现

首先,二叉堆一般是通过数组来实现。假设第一个元素在数组中的索引为0,则父节点和子节点的位置关系如下:

  • 索引为i的左孩子的索引为 2×i+1
  • 索引为i的右孩子的索引为 2×i+2
  • 索引为i的父节点的索引为 (i-1)/2

如下图所示:

heap-2

2.1. 插入操作

插入一个元素的时候,首先将其放置在数组的末尾,然后逐步向上调整,使整棵树重新满足的性质。这里的向上调整,指的是,将新插入的元素与其父节点进行比较,如果其值大于父节点的值,则交换两者的位置。如此迭代,直到该节点变为根节点或者其值不在大于父节点。

insert

2.2. 删除操作

删除堆顶元素时,首先将堆尾元素替换至堆顶,这就相当于把堆顶元素删除了。随后,从堆顶元素开始,由上至下调整堆结构,使其重新满足堆的结构。

这里的向下调整,指的是,将父节点与其两个孩子节点进行比较,并与较大的孩子节点进行交换,逐层向下。

delete

2.3. 初始化

初始化操作将一个普通数组转化成符合堆性质的数组。对于一个原始的二叉堆,从最后一个非空节点(即下标为 (n-1)/2 )开始,逐个向下调整,从而使得整个二叉堆满足大顶堆的性质。

代码

  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
 96
 97
 98
 99
100
101
102
103
104
105
106
107
package main

import (
	"fmt"
)

type MaxHeap []int

func (h *MaxHeap) Peek() int {
	return (*h)[0]
}

func (h *MaxHeap) Pop() int {
	x := (*h)[0]                 // 取出堆顶元素
	n := len(*h)
	h.swap(0, n-1)             // 将堆尾元素调整至堆顶
	*h = (*h)[:n-1]              // 删除末尾节点
	h.heapifyDown(0, len(*h))  // 由上至下调整
	return x
}

func (h *MaxHeap) Push(x int) {
	*h = append(*h, x)
	h.heapifyUp(h.Size() - 1)
}

func (h *MaxHeap) Size() int {
	return len(*h)
}

func (h *MaxHeap) IsEmpty() bool {
	return h.Size() == 0
}

// 将普通的数组调整至符合堆性质
func (h *MaxHeap) Init() {
	n := h.Size()
	// 从最后一个非叶节点开始逐个向下调整
	for i := (n - 1) / 2; i >= 0; i-- {
		h.heapifyDown(i, n)
	}
}

// 0 ≤ i < n
func (h *MaxHeap) Remove(i int) int {
	n := h.Size()
	if i == n-1 { // 如果删除的是最后一个元素,则不需要调整
		x := (*h)[i]
		*h = (*h)[:n-1]
		return x
	}
	x := (*h)[i]
	h.swap(i, n-1)
	*h = (*h)[:n-1]
	h.heapifyDown(i, len(*h))
	return x
}

func (h *MaxHeap) heapifyDown(i, n int) {
	for {
		left, right := 2*i+1, 2*i+2
		j := left // j 表示待交换的孩子节点
		// 判断节点i是否有孩子节点
		if left >= n || left < 0 { // left < 0 after int overflow
			break
		}
		// 如果右节点存在,并且右节点的值更大
		if right < n && (*h)[right] > (*h)[left] {
			j = right
		}
		// 如果父节点i的值大于孩子节点的值,说明不需要交换
		if (*h)[i] > (*h)[j] {
			break
		}
		// swap
		h.swap(i, j)
		i = j // 向下一层
	}
}

func (h *MaxHeap) heapifyUp(i int) {
	for {
		parent := (i - 1) / 2
		if parent < 0 || (*h)[parent] >= (*h)[i] {
			break
		}
		h.swap(i, parent)
		i = parent // 向上一层
	}
}

func (h *MaxHeap) swap(i, j int) {
	(*h)[i], (*h)[j] = (*h)[j], (*h)[i]
}

func main() {
	maxHeap := MaxHeap{1, 3, 5, 4, 2, 7}
	maxHeap.Init()
	maxHeap.Remove(1) // remove 4
	fmt.Printf("top:%d\n", maxHeap.Peek())
	fmt.Printf("%v\n", maxHeap)
	//maxHeap.Push(9)
	//maxHeap.Remove(0)
	for !maxHeap.IsEmpty() {
		fmt.Printf("%d ", maxHeap.Pop())
	}
}

从代码可以看出,最主要的逻辑就是heapifyDown()heapifyUp()这两个函数。理解了这个调整的过程,堆的原理就不难理解了。


参考:

  1. https://en.wikipedia.org/wiki/Heap_(data_structure)
  2. https://golang.org/src/container/heap/heap.go