第14章. 堆
一、堆的引入
现在我们想专门设计一种数据结构,用来存放整数,要求提供3个接口:
- 添加元素
- 获取最大值(或最小值)
- 删除最大值(或最小值)
有一种最优的数据结构就是堆。
时间复杂度:获取最大值的:O(1)、删除最大值O(log n)、添加元素O(log n)
二、堆的相关概念
堆(Heap是一种树状的数据结构),我们只学习二叉堆(也叫完全二叉堆),堆实际就是在完全二叉树的基础上进行了一些调整,堆结构就是用数组实现的完全二叉树结构。
堆的一个重要性质:任意节点的值总是>=(或<=)子节点的值:
- 如果任意节点的值总是大于等于子节点的值,称为最大堆、大根堆、大顶堆。
- 如果任意节点的值总是小于等于子节点的值,称为最小堆、小根堆、小顶堆。
- 堆必须是完全二叉树
- 每一棵树的根节点必须小于/大于左右孩子结点
- 在堆中并不意味着,上一层节点的值一定大于下一层节点的值
- 最大堆的最大值肯定在根节点处,最小堆的最小值也是在根节点处
- 堆中的元素必须具备可比较性
三、二叉堆
- 二叉堆的逻辑结构就是一棵完全二叉树,所以也叫完全二叉堆。
- 鉴于完全二叉树的一些特性,二叉堆的底层(物理结构)一般用数组实现即可。
- 索引 i 的规律(n是节点数量):
- 如果 i = 0,它是根节点
- 如果 i > 0,它的父节点的索引为 floor((i - 1) / 2)
- 如果 2 × i + 1 <= n - 1,它的左子节点的索引为 2 × i + 1
- 如果 2 × i + 1 > n - 1,它无左子节点
- 如果 2 × i + 2 <= n - 1,它的右子节点的索引为 2 × i + 2
- 如果 2 × i + 2 > n - 1,它无右子节点
因为二叉堆是用数组存储的,索引是0 ~ n-1,所以当2 × i + 1 <= n - 1时,它的左子节点存在,索引位2 × i + 1。
四、大根堆——插入操作
流程:
- 循环执行以下操作(图中的80简称为node节点):
- 如果node节点 > 父节点:node节点与父节点交换位置
- 如果node节点 <= 父节点,或者node节点没有父节点(已到达根节点):退出循环
这个过程叫做上溢(上滤),时间复杂度:O(logn)。
private void heapInsert(int[] arr, int index) {
// 当前元素arr[index],当前元素的父结点arr[(index - 1) / 2]
// 退出会有两种情况:arr[index]不必arr[父]大了、index来到了树的根节点位置
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
交换位置的优化。
private void siftUp(int index) {
E element = elements[index];
while (index > 0) {
int parentIndex = (index - 1) >> 1;
E parent = elements[parentIndex];
if (compare(parent, element) >= 0) break;
elements[index] = parent;
index = parentIndex;
}
elements[index] = element;
}
五、大根堆——删除操作
流程:
- 用最后一个节点覆盖根节点
- 删除最后一个节点(也就是删除堆顶元素)
- 循环执行以下操作(图中的43简称为node节点)
- 如果node节点 < 子节点:与最大的子节点交换位置
- 如果node节点 >= 子节点,或者node没有子节点:退出循环
这个过程叫做下滤(Sift Down),时间复杂度:O(logn)
同样的,交换过程也可以像删除过程一样进行优化。
// 从index位置,往下看,不断地下沉
// 停:我的孩子都不再比我大;已经没有孩子了
private void shifDown(int[] arr, int index. int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {
// 左右两个孩子中,谁大,谁把自己的下标给largest
// 右孩子大--> 1)有右孩子 2)右孩子的值比左孩子大
// 否则都是左孩子大
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
// 最大值与index(父节点)进行比较
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) break;
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
六、最大堆——批量建堆(heapify)
- 批量建堆,有2种做法:
- 自上而下的上滤
- 自下而上的下滤(效率比较高)
自上而下的下滤
// 根节点不用上滤,所以索引从1开始即可
for (int i = 1; i < size; i ++) {
siftUp(i);
}
自下而上的下滤
// 从最后一个非叶子节点开始
for (int i = (size >> 1) - 1; i >= 0; i --) {
siftDown(i);
}
效率对比
七、优先队列(Priority Queue)
- 普通的队列是FIFO原则,也就是先进先出
- 优先级队列是按照优先级高低进行出队,比如将优先级最高的元素作为队头优先出队
- 根据优先队列的特点,很容易想到:可以直接利用二叉堆作为优先队列的底层实现
- 可以通过Comparator 或 Comparable去自定义优先级高低
八、LeetCode相关题目
- LeetCode23. 合并K个升序链表
- LeetCode215. 数组中的第K个最大元素
- LeetCode703. 数据流中的第K大元素
本文作者:Ac_c0mpany丶
本文链接:https://www.cnblogs.com/keyongkang/p/17880521.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步