heap 包源码阅读
最近在练习leetcode题目时,需要用到堆,发现 Golang 标准库已经有heap
包,在使用的同时也看看源码的写法。
1. 堆的定义
In computer science, a heap is a specialized tree-based data structure that satisfies the heap property: In a max heap, for any given node C, if P is a parent node of C, then the key (the value) of P is greater than or equal to the key of C. In a min heap, the key of P is less than or equal to the key of C.
2. 数据结构
堆通常使用完全二叉树来实现并且完全二叉树可以使用数组来表示,故堆可以使用数组来实现。
堆的类型通常有两种:
- 最大堆:父节点的元素一定大于等于子节点
- 最小堆:父节点的元素一定小于等于子节点
数组中节点的关系
在用数组表示的堆中,假设当前节点为,i
为数组索引,则有:
- 父节点索引为:
- 左子节点索引为:
- 右子节点索引为:
3. 接口
heap
包中的函数都是以接口类型的变量作为参数,这样做的好处:
- 调用方可以通过接口与具体实现分离,解除上下游的耦合
- 上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口
- 无需关心函数的具体实现,只要实现了约定的接口即可
heap
包只有一个结构heap.Interface
:
type Interface interface {
sort.Interface
Push(x any) // add x as element Len()
Pop() any // remove and return element Len() - 1.
}
将其中的sort.Interface
展开后可以得到:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
Push(x any)
Pop() any
}
也就是说实现heap.Interface
需要实现上述的5个方法,因为堆是由数组实现的,所以实现的方法是针对数组及其元素:
Len
:返回数组的大小Less
:比较数组中元素的大小Swap
:交换数组中元素Push
:向数组中添加元素Pop
:返回并删除数组的最后一个元素
4. 函数
以最小堆为例进行分析。
4.1 up(h, j) and down(h, i0, n)
heap.up
和heap.down
是包中最核心的函数,也是堆的最基础的操作:
up
:上浮,表示当前节点和其父节点交换;发生在新增节点时(Push),此时节点在二叉树的最低层,判断是否需要上浮。down
:下沉,表示当前节点与其子节点交换;发生在删除栈顶节点(Pop)时,将堆中的最后一个节点放到堆顶,判断是否需要下沉。
down
时间复杂度:,n 为数组长度(堆的大小);当节点需要交换至二叉树底部时,交换次数为二叉树高度h
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
}
流程如下:
- 将传入的索引记为当前索引
- 获取左节点索引,若左节点不存在(左节点索引大于数组长度 或 数值溢出),则跳转至 步骤 7)
- 获取右节点索引,若右节点存在且元素值小于左节点,则选择右节点
- 比较当前节点,若不小于右节点,则跳转至 步骤 7);
- 交换当前当前节点和子节点(左右节点中较小的一个)
- 更新当前节点索引,继续 步骤 1)
- 返回当前节点索引是否大于传入索引
总体流程:
- 将当前节点和最小的子节点交换,直到无法交换为止
- 返回是否发生了下沉(节点交换)
up
时间复杂度:,n 为数组长度(堆的大小);当节点需要交换至二叉树顶部时,交换次数为二叉树高度h
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
}
}
流程:
- 计算父节点的索引
- 当前已经是堆顶 或 不小于父节点,跳转至步骤 5)
- 交换当前节点和父节点
- 更新当前节点,继续 步骤1)
- 结束
总体流程:
- 和父节点交换,直到堆顶 或者 不满足交换条件
4.2 Init(h)
初始化堆,当定义的数组存在初始元素是可以调用函数Init
将其堆化(heapify),即转化成堆。
func Init(h Interface) {
// heapify
n := h.Len()
for i := n/2 - 1; i >= 0; i-- {
down(h, i, n)
}
}
此算法的时间复杂为。
为什么时间复杂度是 O(n)
详细证明可以参见How can building a heap be O(n) time complexity?。
假设树的高度,那么构建堆的交换次数计算如下:
证明过程:
4.3 Pop and Push
时间复杂度均为 O(logn)
func Push(h Interface, x any) {
h.Push(x)
up(h, h.Len()-1)
}
func Pop(h Interface) any {
n := h.Len() - 1
h.Swap(0, n)
down(h, 0, n)
return h.Pop()
}
4.4 Remove
删除指定位置的元素,时间复杂度:O(logn)
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()
}
总体流程:
- 将想要删除的元素和最后一个元素交换
- 对最后一个元素进行上浮/下沉操作
- 删除最后一个元素(值为想要删除的元素)
4.5 Fix
当元素的值(或者说优先级)被修改,调整其在堆中的位置,时间复杂度:O(logn)
func Fix(h Interface, i int) {
if !down(h, i, h.Len()) {
up(h, i)
}
}