一、堆的概念
- 堆逻辑上是一棵完全二叉树
- 堆物理上是保存在数组中
- 满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆
- 满足任意结点的值都小于其子树中结点的值,则是小堆,或者小根堆,或者最小堆
- 堆的基本作用是,快速找集合中的最值
二、堆的操作
向下调整(以小堆为例)
思路:伪代码分析
public static void 向下调整(int[] array, int size, int index) {
1.判断 index 对应的下标是不是叶子结点
如果是叶子结点,直接 return
2.找到两个孩子中最小的
3.最小的孩子的值和 index 对应位置的值比较
< 接着进行下一步
== > 满足堆的性质,直接 return
4.交换最小的孩子的值和 index 的值
5.把最小的孩子视为 index,循环回去(从步骤 1,继续往下走)
}
引出的问题:
- 怎么判断 index 对应的位置是不是叶子结点 ?
- 怎么找最小的孩子?
- 小堆是有序的吗?有序的是小堆吗?
小堆不一定是有序的,但有序的一定是小堆。
代码实现:
public class HeapTest {
public static void shiftDown(int[] array, int size, int index) {
while (true) {
//1.判断 index 对应的下标是不是叶子结点
int leftIndex = 2 * index + 1;
if(leftIndex >= size) {
return;
}
//2.找到两个孩子中最小的
int minIndex = leftIndex;
int rightIndex = leftIndex + 1;
if(rightIndex < size && array[rightIndex] < array[leftIndex]) {
minIndex = rightIndex;
}
//3.最小的孩子的值和 index 对应位置的值比较
if(array[index] <= array[minIndex]) {
return;
}
//4.交换最小的孩子的值和 index 的值
int temp = array[index];
array[index] = array[minIndex];
array[minIndex] = temp;
//5.把最小的孩子视为 index,循环回去(从步骤 1,继续往下走)
index = minIndex;
}
}
}
向上调整(以大堆为例)
思路:伪代码分析
public static void adjustUp(int[] array,int size,int index) {
1.判断 index 是不是树的根,如果是根,调整结束
2.找到 index 的父结点
3.比较父结点的值和 index 的值
4.只要父结点的值 <= index,调整结束
5.交换父结点和 index 的值
6.把父结点看做 index,继续这个大循环
}
代码实现:
public class HeapTest {
public static void adjustUp(int[] array,int size,int index) {
while (true) {
//1.判断 index 是不是树的根,如果是根,调整结束
if(index == 0) {
break;
}
//2.找到 index 的父结点
int parentIndex = (index - 1) / 2;
//3.比较父结点的值和 index 的值
//4.只要父结点的值 <= index,调整结束
if(array[parentIndex] <= array[index]) {
break;
}
//5.交换父结点和 index 的值
int temp = array[index];
array[index] = array[parentIndex];
array[parentIndex] = temp;
//6.把父结点看做 index,继续这个大循环
index = parentIndex;
}
}
}
建堆
思路:伪代码分析
public static void createHeap(int[] array, int size) {
size-1 树最后一个结点的下标
((size-1)-1)/2
从[(size-2)/2,0] 不断地进行向下调整
}
代码实现:
public static void createHeap(int[] array, int size) {
//找到层序遍历的最后一个结点下标
int lastIndex = size - 1;
//找到最后一个结点的父节点的下标
int lastParentIndex = lastIndex - 1 / 2;
//从[(size-2)/2,0] 不断地进行向下调整
for(int i = lastParentIndex; i >= 0; i++) {
shiftDown(array,size,i);
}
}
三、堆的应用
堆的应用有挺多:优先级队列、堆排序、TopK。
堆排序和TopK就先不浅析了,会写博客专门去分析理解,在这里就简要分析一下。
堆排序
分为两个步骤:
- 用当前需要排序的数据构建一个堆
- 不断的弹出当前堆的堆顶元素,因为小顶堆的堆顶元素一定是最小的,即可以用于排序。堆排序的本质就是,把数据构建成堆之后,弹出堆顶元素,然后互换堆顶元素和最后一个元素,不断对当前堆进行自顶向下的堆的调整,然后继续弹出。
TopK
TopK一般解决的是求解前K个最大或者最小的元素,或第K个最大或最小的元素。
拿到这类问题,我们的第一想法肯定是排序求解。但排序还会浪费一定的资源排序前K个元素,为了节省计算资源,我们需要思考的是怎么优化,聪明的你肯定想到了,是不是这最大的k个元素也不需要排序呢?
为此,我们需要构建包含K个元素的小顶堆,这个小顶堆用于存储,当前最大的k个元素。接着我们需要从第 k+1个元素开始扫描,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。扫描完所有n-k个元素,最终堆中的k个元素,就是优化后求的TopK。
优先级队列(Priority Queue)
入队列操作
过程(以大堆为例):
出队列操作
过程(以大堆为例):
代码实现
public class MyPriorityQueue {
private Integer[] array;
private int size;
public MyPriorityQueue() {
array = new Integer[100];
size = 0;
}
public Integer element() {
if(size == 0) {
throw new RuntimeException("空的");
}
return array[0];
}
/**
* 入队列
*/
public void add(Integer e) {
array[size] = e;
size++;
adjustUp(size-1);
}
public void adjustUp(int index) {
while (true) {
if(index == 0) {
break;
}
int parentIndex = (index - 1) / 2;
if(array[parentIndex] <= array[index]) {
break;
}
int temp = array[index];
array[index] = array[parentIndex];
array[parentIndex] = temp;
index = parentIndex;
}
}
/**
* 出队列
*/
public Integer remove() {
if(size == 0) {
throw new RuntimeException("空的");
}
int e = array[0];
array[0] = array[size - 1];
size--;
adjustDown(0);
return e;
}
private void adjustDown(int index) {
while (true) {
int leftIndex = 2 * index + 1;
if(leftIndex >= size) {
return;
}
int minIndex = leftIndex;
int rightIndex = leftIndex + 1;
if(rightIndex < size && array[rightIndex] < array[leftIndex]) {
minIndex = rightIndex;
}
if(array[index] <= array[minIndex]) {
return;
}
int temp = array[index];
array[index] = array[minIndex];
array[minIndex] = temp;
index = minIndex;
}
}
}
测试类:
public class PriorityQueueDemo {
public static void main(String[] args) {
MyPriorityQueue queue = new MyPriorityQueue();
queue.add(3);
queue.add(5);
queue.add(2);
queue.add(7);
System.out.println(queue.remove());//2
System.out.println(queue.remove());//3
System.out.println(queue.remove());//5
System.out.println(queue.remove());//7
}
}
运行结果: