优先级队列(堆)
1. 堆的顺序存储
1.1 堆的概念
-
堆逻辑上是一棵完全二叉树
-
堆物理上是保存在数组中比特科技
-
满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆
-
反之,则是小堆,或者小根堆,或者最小堆
-
堆的基本作用是,快速找集合中的最值
1.2 存储方式
使用数组保存二叉树结构,方式是将二叉树用层序遍历方式放入数组中。
一般只适合表示完全二叉树,因为非完全二叉树会有空间的浪费。
这种方式的主要用法就是堆的表示。
1.3 下标关系
已知双亲**(parent)**的下标,则:
左孩子(left)下标 = 2 * parent + 1;
右孩子(right)下标 = 2 * parent + 2;
**已知孩子(不区分左右)****(child)**下标,则:
双亲(parent)下标 = (child - 1) / 2;
2. 堆的相关操作
2.1 向下调整
-
index 如果已经是叶子结点,则整个调整过程结束
- 判断 index 位置有没有孩子
- 因为堆是完全二叉树,没有左孩子就一定没有右孩子,所以判断是否有左孩子
- 因为堆的存储结构是数组,所以判断是否有左孩子即判断左孩子下标是否越界,即 left >= size 越界
-
确定 left 或 right,谁是 index 的最小孩子 min
- 如果右孩子不存在,则 min = left
- 否则,比较 array[left] 和 array[right] 值得大小,选择小的为 min
-
比较 array[index] 的值 和 array[min] 的值,如果 array[index] <= array[min],则满足堆的性质,调整结束
-
否则,交换 array[index] 和 array[min] 的值
-
然后因为 min 位置的堆的性质可能被破坏,所以把 min 视作 index,向下重复以上过程
时间复杂度分析:
最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度
即时间复杂度为 O(log(n))
代码:
public void adjustDown(int arr[], int index, int size) {
while (index * 2 + 1 < size) {
//当需要调整的元素不存在或者它没有孩子时,直接返回
//找到孩子中较小的
int min;
if (index * 2 + 2 >= size || arr[index * 2 + 1] > arr[index * 2 + 2]) {
min = index * 2 + 1;
} else {
min = index * 2 + 2;
}
//将min和arr[index]比较
if (arr[index] > arr[min]) { //如果当前元素更大, 就向下置换
{int temp = arr[index]; arr[index] = arr[min]; arr[min] = temp;}
}
index = min;
}
}
2.2 建堆
思路:从倒数的第一个非叶子节点的子树开始调整,一直 调整到根节点的树,就可以调整成堆。
public void createHeap(int arr[]) {
//思路: 將每个非叶子结点都向下调整
// 最后一个结点的父结点下标设为par 需要调整的区间就是[0,par]
int par = (arr.length - 2) / 2;
for (int i = par; i >= 0; i--) {
adjustDown(arr, i, arr.length);
}
}
2.3 向上调整
相对简单,不多做赘述
public void adjustUp(int arr[], int index, int size) {
//将该元素与他的父结点比较, 如果小于父结点 就向上置换
while (true) {
int par = (index - 1) / 2; // 父结点的下标
if (par <= 0 || arr[par] < arr[index]) {
return;
}
int temp = arr[index]; arr[index] = arr[par]; arr[par] = temp;
index = par;
}
}
3. 堆的相关应用
-
优先级队列 (java.util.PriorityQueue)
-
排序 (堆排序)
-
TopK(经典面试题)
问题:选出最大或最优秀的k个元素
思路:建立K个元素的小根堆,然后依次向后遍历,每个元素和堆顶元素比较(挑战) 如果大于就替换,然后向下调整