堆的概念
堆就是利用数组作为底层实现的完全二叉树,堆根据跟节点的性质可以分为两种
- 大根堆:根节点为整个二叉树中值最大的节点,且需要满足任何一子树的跟节点值都大于自身的孩子节点的值
- 小根堆:与大根堆定义类似,根节点为二叉树中值最小的节点,且需要满足任何一株子树的跟节点值都小于孩子的节点的值
需要注意的是,堆只有根节点可以认定为是整个树中的最大值或者最小值,不能根据其左右子树来判定节点值的大小。
堆底层数组与节点的映射
堆是用数组来实现的,可以根据层序遍历的方式将一个数组还原成为一棵完全二叉树。
可以使用数组下标直接找到该节点的叶子节点的位置,数组中下标为0的元素即为树的根节点
- 元素下标为 i
- 左节点:2*i + 1
- 右节点:2*i + 2
需要注意的是,在查询左右节点时,需要判定计算出的位置是否超出数组的范围。
初始化堆
给定一个初始的数组,将该数组初始化为堆数组的流程如下:(以小根堆为例)
- 采用的方式为遍历所有的非叶子节点,将该节点值与其左右孩子节点做比较,根据比较的值进行下一步操作,根据完全二叉树的概念,第一非叶子节点的下标为:n/2 - 1
- 根节点与两个孩子节点中的最小值比较,若根节点的大于该值,则交换根节点与最小值对应的孩子节点,并对以交换后的孩子节点对应的子树执行比较操作。若未发生交换,则继续下一步比较操作。
- 当所有的比较操作完成之后,该小根堆即构建完成。
- Init方法中调用的down方法,在后面的获取最小值方法中,也会使用到。
初始化堆的时间复杂度是O(n)
func Init(arr []int) {
n := len(arr)
for i := n/2 - 1; i >= 0; i-- {
down(arr, i, n)
}
}
// 将根节点上的元素放置到子树中正确的位置
func down(arr []int, i, len int) {
for {
temp := 2*i + 1 // 与根节点比较值的孩子节点下标
if temp >= len || temp < 0 {
break
}
// 当存在右子节点且右子节点的值小于左子节点时,根节点与右子节点比较
if a := temp + 1; a < len && arr[a] < arr[temp] {
temp = a
}
// 若根节点的值小于叶子节点的值,则该子树构建完成
if arr[i] < arr[temp] {
break
}
swap(arr, i, temp)
i = temp
}
}
// 交换数组中两个元素
func swap(arr []int, i, j int) {
temp := arr[i]
arr[i] = arr[j]
arr[j] = temp
}
新增数据
向堆中新增一个数据时,需要调整堆,使其维持最小堆的形态。
新增数据的时间复杂度为 O(log(n)) , n为底层数组的长度
- 将新元素添加到底层数组的末尾
- 将末尾元素与其父节点的值进行比较,若新元素值大于父节点的值,则堆形态未被破坏,不需要调整。
- 将末尾元素与其父节点的值进行比较,如果新元素的值小于父节点的值,则交换节点的位置,然后继续比较新元素当前位置与其新父节点的值,以此循环。
func Push(arr []int, k int) []int {
arr = append(arr, k)
len := len(arr)
// 调整新元素在堆中的位置
up(arr, len-1)
return arr
}
func up(arr []int, i int) {
for {
// j为i位置对应的父节点
j := (i - 1) / 2
if j == i || arr[i] >= arr[j] {
break
}
swap(arr, i, j)
i = j
}
}
删除元素最小值
删除最小元素即为删除堆中的根节点,然后重新构建堆。为了维持堆的形态,通常的做法是先交换第一个元素和最后一个元素的位置,然后删除最后一个元素,再重新构建堆
- 交换数组的第一个元素与最后一个元素
- 删除数组中的最后一个元素
- 对数组中的第一个元素执行down操作,使其放置到适合的位置
时间复杂度为 O(log(n))
func Pop(arr []int) (int, []int) {
swap(arr, 0, len(arr)-1)
val := arr[len(arr)-1]
arr = arr[:len(arr)-1]
down(arr, 0, len(arr))
return val, arr
}