堆
堆的本质是树,用数组表示的完全二叉树;
定义
一棵深度为k且有 2^k - 1
个结点的二叉树称为满二叉树。
根据二叉树的性质2, 满二叉树每一层的结点个数都达到了最大值, 即满二叉树的第i层上有 2^(i-1)
个结点 (i≥1) 。
如果对满二叉树的结点进行编号, 约定编号从根结点起, 自上而下, 自左而右。则深度为k的, 有n个结点的二叉树, 当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时, 称之为完全二叉树。
从满二叉树和完全二叉树的定义可以看出, 满二叉树是完全二叉树的特殊形态, 即如果一棵二叉树是满二叉树, 则它必定是完全二叉树。
参考: https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91/7773232
性质
arr:[2 3 4 52 2 2 1]
idx: 0 1 2 3 4 5 6
i 下标和元素之间的映射关系:
- 左子节点:
2*i+1
- 右子节点:
2*i+2
- 父节点:
(i-1)/2
大根堆
完全二叉树里,每一个子树的最大值是根节点;
小根堆
完全二叉树里,每一个子树的最小值是根节点;
堆排序
定义堆
// maxHeap 定义一个大根堆
type maxHeap struct {
Data []int
Count int
}
func NewMaxHeap(size int) *maxHeap {
return &maxHeap{
Data: make([]int, size),
Count: 0,
}
}
插入数据
插入数据时,是往数组最后增加元素,由于需要保证大根堆的性质,如果新加入的元素比父节点大,则跟父节点交换位置,以此类推,一直到根节点,这个交换流程完成后,新元素插入就完成了。
父节点下标跟当前下标index的关系:父节点 = (i-1)/2
// insert 向堆中插入元素
func (heap *maxHeap) Insert(val int) {
heap.Data[heap.Count] = val
heap.Count++
// heap.shiftUp(heap.Count)
heap.shiftUp1(heap.Count - 1) // 最后一个元素的下标
}
// shiftUp 向堆中插入元素时,叶子节点可能需要向上移动
func (heap *maxHeap) shiftUp1(index int) {
// 父节点 = (i-1)/2
for index > 0 && heap.Data[index] > heap.Data[(index-1)/2] {
heap.Data[index], heap.Data[(index-1)/2] = heap.Data[(index-1)/2], heap.Data[index]
index = (index - 1) / 2
}
fmt.Printf("shiftUp heap %v \n", heap)
}
// shiftUp 向堆中插入元素时,叶子节点可能需要向上移动
func (heap *maxHeap) shiftUp(count int) {
for count > 1 && heap.Data[count-1] > heap.Data[(count/2)-1] {
heap.Data[count-1], heap.Data[(count/2)-1] = heap.Data[(count/2)-1], heap.Data[count-1]
count = (count / 2)
}
fmt.Printf("shiftUp heap %v \n", heap)
}
移除数据
移除数据,就是移除根节点,同时把最后一个元素放到根节点,然后跟子节点比较,此时,大根堆需要找到左右子节点中,较大的一个元素,跟父节点进行互换,以此类推,直到比较完最后一个节点,同时 count-1
;
// ExtraMax 提取大根堆的最大值,也就是根节点,大根堆整体性质不变
func (heap *maxHeap) ExtraMax() int {
heap.Count--
result := heap.Data[0]
heap.Data[0], heap.Data[heap.Count] = heap.Data[heap.Count], heap.Data[0]
heap.shiftDown(0)
return result
}
// shiftDown 移除堆中元素,节点向下移动
func (heap *maxHeap) shiftDown(index int) {
for index <= heap.Count-1 {
i := heap.getMaxChildNode(index)
if i == -1 {
break
}
if heap.Data[index] < heap.Data[i] {
heap.Data[index], heap.Data[i] = heap.Data[i], heap.Data[index]
}
index = i
}
}
// getMaxChildNode 获取最大子节点下标,如果没有子节点,return -1
func (heap *maxHeap) getMaxChildNode(index int) int {
if index <= heap.Count-1 {
if index*2+2 > heap.Count-1 {
// 当右节点下标越界时
if index*2+1 <= heap.Count-1 {
// 如果左下标没有越界,则返回左节点下标
return index*2 + 1
}
} else {
// 当右节点下标没有越界时,此时左下标一定没有越界
if heap.Data[index*2+1] >= heap.Data[index*2+2] {
return index*2 + 1
} else {
return index*2 + 2
}
}
}
// 没有子节点时,返回-1
return -1
}
堆初始化
堆的内部实际是一个数组,数组可以通过下标表示为树形结构,堆初始化可以把数组转化为大根堆或者小根堆,这里需要借助完全二叉树的性质:
最后一个非叶子节点是:(n-1)/2
(n 是数组大小)
// MaxHeapipy 最大堆初始化
func MaxHeapipy(arr []int) *maxHeap {
mh := NewMaxHeap(len(arr))
mh.Data = arr
mh.Count = len(arr)
// 堆是一棵完全二叉树,最后一个非叶子节点是:(n-1)/2,n 是数组大小
// 基于这个性质,可以依次对每一个非叶子节点进行 shiftDown,相当于每一个子树都完成 shiftDown,
// 最后完成根节点的 shiftDown
// shiftDown 之后,依然保持大根堆的性质
for i := (mh.Count - 1) / 2; i >= 0; i-- {
mh.shiftDown(i)
}
return mh
}
堆排序扩展
已知一个几乎有序的数组,几乎有序是指,如果把数组排好序的话,每个元素移动的距离不可以超过k,并且k相对于数组来说比较小。请选择一个合适的算法针对这个数据进行排序。
假设k=6,准备一个小根堆,遍历数组,把前7个元素构建一个小根堆,则0位置是数组的最小值(因为排好序之后,每个元素移动距离不会超过k),把小根堆的根节点弹出,放到数组的0位置,然后依次把7个元素之后的数据插入到小根堆,弹出根节点的数据,弹出的数据一次有序。