Heap

堆(Heap)是一种完全二叉树结构,常用于快速取得当前的最大值或最小值。它不是完全排序,而是 partial ordered

堆只保证父子之间的局部有序,并不保证兄弟节点或不同子树之间有序。它和 binary search tree 的区别是:

正因为只维护这种局部顺序,堆可以高效支持:

在算法题里,堆通常作为 priority queue 使用,用来动态维护“当前最小/最大”的元素,比如:

由于堆是完全二叉树,所以通常直接用数组表示,而不需要真的定义树节点。

Build Heap with array

对 0-based array 来说,如果某个节点的下标是 k,那么:

为什么会是这个公式?

因为完全二叉树每一层都是从左到右紧密排列的。第 i 层一共有 2^i 个节点,而前面所有层的节点总数是:

2^i - 1

如果某个节点位于第 i 层、并且它是这一层从左往右第 d 个节点,那么它在数组中的下标就是:

k = (2^i - 1) + d

它的左孩子位于下一层,并且仍然对应第 2d 个位置,所以左孩子下标为:

(2^(i+1) - 1) + 2d = 2((2^i - 1) + d) + 1 = 2k + 1

同理,右孩子下标就是 2k + 2

func buildHeap(nums []int) {
	size := len(nums)

	// k, 2k+1, 2k+2;  k = (i-1)/2
	for i := size/2 - 1; i >= 0; i++ {
		siftDown(nums, i)
	}
}

func pop(heap []int) int {
	// switch to end and sift down
	item := heap[0]

	heap[0] = heap[len(heap)-1]
	heap = heap[:len(heap)-1]
	siftDown(heap, 0)

	return item
}

func push(heap []int, item int) []int {
	// append to end and sift up
	heap = append(heap, item)
	siftUp(heap, len(heap)-1)

	// here we created new slice and return it
	return heap
}

func siftUp(nums []int, i int) {
	if i <= 0 {
		return
	}

	parent := (i - 1) / 2
	if nums[parent] > nums[i] {
		nums[i], nums[parent] = nums[parent], nums[i]
		siftUp(nums, parent)
	}
}

func siftDown(nums []int, i int) {
	size := len(nums)

	smallest := i
	l, r := 2*i+1, 2*i+2
	if l < size && nums[l] < nums[smallest] {
		smallest = l
	}
	if r < size && nums[r] < nums[smallest] {
		smallest = r
	}

	if smallest != i {
		nums[i], nums[smallest] = nums[smallest], nums[i]
		siftDown(nums, smallest)
	}
}

Heap Building & Sift Process

堆的构建通常采用 siftDown(下沉)操作,从最后一个非叶子节点开始,自底向上进行堆化。其时间复杂度为 $O(n)$。

SiftDown 过程

  1. 比较当前节点与左右子节点的值。
  2. 在三者中找到最小(小顶堆)或最大(大顶堆)的节点。
  3. 如果最小/最大节点不是当前节点,则交换它们,并对交换后的子节点递归执行 siftDown

堆构建流程可视化 (Min-Heap)

[7, 2, 5, 8, 1, 6, 4] 为例,堆构建从最后一个非叶子节点(index = 2, value = 5)开始:

D2 Diagram
qtopie.github.io

Go Heap lib

import (
	"container/heap"
	"fmt"
)

type Cell struct {
	dx     int
	dy     int
	height int
}

type PriorityQueue []*Cell

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

func (pq PriorityQueue) Less(i, j int) bool {
	return pq[i].height < pq[j].height
}

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

func (pq *PriorityQueue) Push(x any) {
	item := x.(*Cell)
	*pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() any {
	n := len(*pq)
	item := (*pq)[n-1]
	*pq = (*pq)[0 : n-1]
	return item
}

func main() {
	cells := [][3]int{
		{0, 0, 7},
		{0, 1, 2},
		{0, 2, 5},
	}

	pq := make(PriorityQueue, 3)
	for i := 0; i < 3; i++ {
		pq[i] = &Cell{cells[i][0], cells[i][1], cells[i][2]}
	}
	heap.Init(&pq)

	for pq.Len() > 0 {
		val := heap.Pop(&pq)
		fmt.Println(val)
	}

}

implementation source heap.go

Reference