目录
一、堆及其概念
堆在逻辑上是一颗完全二叉树,在物理上保存于数组。满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆反之,则是小堆,或者小根堆,或者最小堆。堆的基本作用是在频繁变动的集合中找到最值。
二、向下调整(向上调整)
前提:调整的左右节点必须是堆。才能调整。
基本思想:
1.判段index位置是否是叶子节点
2.找到该节点的两个孩子的最值(需要知道是否有孩子)
3.将最值和当前需要调整的位置进行比较,判断是否满足堆的性质
4.交换两个值
5.由于交换的原因,调整过后的节点可能不满足堆的性质,继续调整
public static void shiftDown(long[] array, int size, int index) {
// 只要看到 int 类型的,基本就是下标或者个数,不是元素
// 这里直接 while(true)即可
// while (2 * index + 1 < size) { 如果这么写,下面就不用再进行叶子的判断
while (true) {
// 1. 判断 index 所在位置是不是叶子
// 逻辑上,没有左孩子一定就是叶子了(因为完全二叉树这个前提)
int left = 2 * index + 1;
if (left >= size) {
// 越界 -> 没有左孩子 -> 是叶子 -> 调整结束
return; // 循环的出口一:走到的叶子的位置
}
// 2. 找到两个孩子中的最值【最小值 via 小堆】
// 先判断有没有右孩子
int right = left + 1; // right = 2 * index + 2
int min = left; // 假设最小值就是左孩子,所以 min 保存的最小值孩子所在的下标
if (right < size && array[right] < array[left]) {
// right < size 必须在 array[right] < array[left] 之前,不能交换顺序
// 因为先得确定有右孩子,才有比较左右孩子的意义
// 有右孩子为前提的情况下,然后右孩子的值 < 左孩子的值
min = right; // min 应该是右孩子所在的下标
}
// 3. 将最值和当前要调整的位置进行比较,判断是否满足堆的性质
if (array[index] <= array[min]) {
// 当前要调整的结点的值 <= 最小的孩子值;说明这里也满足堆的性质了,所以,调整结束
return; // 循环的出口一:循环期间,已经满足堆的性质了
}
// 4. 交换两个值,物理上对应的就是数组的元素交换 min 下标的值、index 下标的值
long t = array[index];
array[index] = array[min];
array[min] = t;
// 5. 再对 min 位置重新进行同样的操作(对 min 位置进行向下调整操作)
index = min;
}
}
向上调整后续的例子中介绍
三、堆的建立
我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。
基本思路:从最后一个非叶子节点开始,逐步往前进行进行向下调整。代码如下
public static void createHeap(long[] array, int size) {
// 从最后一个非叶子结点的双亲开始
// 最后一个结点的下标一定是: size - 1
// 它的双亲一定是: ((size - 1) - 1) / 2 = (size - 2) / 2
// 从后往前遍历,直到根也被向下调整过
// [(size - 2) / 2, 0] 左闭右闭
for (int i = (size - 2) / 2; i >= 0; i--) {
shiftDown(array, size, i);
}
}
其中shiftDown操作为向下调整。
四、优先级队列
我们通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象,然后处理次 高的对象。在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这 种数据结构就是优先级队列(Priority Queue)。优先级队列的实现方式有很多,但最常见的是使用堆来构建。
我们优先级队列的具体使用方式如下
import java.util.Comparator;
import java.util.PriorityQueue;
public class Demo3 {
public static void main(String[] args) {
/*
PriorityQueue<Integer> pq=new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return -o1.compareTo(o2);
}
});
*/
PriorityQueue<Integer> pq=new PriorityQueue<>(((o1, o2) ->-o1.compareTo(o2)));
pq.offer(1);
pq.offer(2);
pq.offer(3);
pq.offer(4);
pq.offer(5);
Integer peek = pq.peek();
System.out.println(peek);
System.out.println("=========================================");
while (!pq.isEmpty()){
Integer poll = pq.poll();
System.out.println(poll);
}
}
}
我们重写了比较器,这样就可以按照我们的要求来进行操作。具体采用了两种,一种是匿名类的写法,还有一种是lambda表达式。
五、例题
1.top-k问题
思路:这题的解法较多,不在此处赘述。我们采用堆的方法来处理。并且为了巩固练习向下调整等操作。
1.将数组的前K个元素建立成为一个大堆
2.从K+1个元素开始,与根节点进行比较,如果比根节点小,那么我们用这个值替换掉根节点。然后进行调整。
3.循环完成后输出这个堆,此时得到的就是最小的K个数。
import java.util.*;
class Solution {
public void adjust_heap(int[] heap,int t){
if(heap[0]>t){
heap[0]=t;
shiftDown(heap,heap.length,0);
}
}
public int[] smallestK(int[] arr, int k) {
if(arr.length==0||k==0){
return new int[0];
}
int[] heap=makeHeap(arr,k);
for(int i=k;i<arr.length;i++){
adjust_heap(heap,arr[i]);
}
return heap;
}
public int[] makeHeap(int[] arr,int k){
int[] list=Arrays.copyOf(arr,k);
for(int i=(k-2)/2;i>=0;i--){
shiftDown(list,k,i);
}
return list;
}
public void shiftDown(int[] arr,int size,int i){
while(true){
int left=2*i+1;
if(left>=size){
return;
}
int right =left+1;
int max=left;
if(right<size&&arr[right]>arr[left]){
max=right;
}
if(arr[i]>=arr[max]){
return;
}
int t =arr[i];
arr[i]=arr[max];
arr[max]=t;
i=max;
}
}
}
2.最后一块石头的重量
思路:目的是为了练习优先级队列。所以我们采用优先级队列将重量全部接收。然后抛出最大的两个值,如果这两个值不相等,我们就再接收它们的差值。
class Solution {
public int lastStoneWeight(int[] stones) {
PriorityQueue<Integer> pq=new PriorityQueue<>(((o1, o2) -> o2-o1));
for (int stone : stones) {
pq.offer(stone);
}
while(pq.size()>1){
int a=pq.poll();
int b=pq.poll();
if(a>b){
pq.offer(a-b);
}
}
return pq.isEmpty() ? 0:pq.poll();
}
}