【JAVA数据结构系列】04_堆详解

1.优先级队列

1.1 概念

前面介绍过队列, 队列是一种先进先出(FIFO)的数据结构,但有些情况下, 操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,这种场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如 果有来电,那么系统应该优先处理打进来的电话。
在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。 这种数据结构就是优先级队列(Priority Queue)

在这里插入图片描述

在这里插入图片描述

2.优先级队列的模拟实现

JDK1.8中的PriorityQueue底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调 整。

2.1 堆的概念

堆:是在完全二叉树的基础上进行了条件的限制,即:每个节点都比其孩子节点大,则为大堆;每个节点都比其孩子节点小则为小堆。

  • 完全二叉树比较适合使用顺序结构存储。

  • 如果有一个关键码的集合K ={k0 ,k1 , k2 ,… ,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一 个一维数组中,并满足: Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0 , 1 ,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

在这里插入图片描述

2.2 堆的存储方式

从堆的概念可知, 堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储,
在这里插入图片描述

注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树, 空间中必须要存储空节 点,就会导致空间利用率比较低。
将元素存储到数组中后,可以根据二叉树章节的性质5对树进行还原。假设i为节点在数组中的下标,则有:

  • 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为(i - 1)/2
  • 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1 ,否则没有左孩子
  • 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子

2.3 堆的创建

建堆时,从每一个非叶子节点开始,倒着一直到根节点,都要执行一次向下调整算法。

2.3.1 堆向下调整

对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?

在这里插入图片描述

仔细观察上图后发现: 根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。

向下过程(以小堆为例):

  • 1、让parent标记需要调整的节点, child标记parent的左孩子(注意: parent如果有孩子一定先是有左孩子)
  • 2、 如果parent的左孩子存在,即:child < size , 进行以下操作,直到parent的左孩子不存在
    • parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标
    • 将parent与较小的孩子child比较,如果:
      • parent小于较小的孩子child,调整结束
      • 否则:交换parent与较小的孩子child,交换完成之后, parent中大的元素向下移动,可能导致子 树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续2。调整:向下调整。从每棵子树的根节点开始进行调整。每棵子树的调整属于向下调整。

在这里插入图片描述

2.3.2 创建大根堆

在这里插入图片描述


在这里插入图片描述

  //建堆:大根堆
  public void createHeap(){
      for(int parent=(usedSize-1-1)/2;parent>=0;parent--){
          shiftDown(parent,usedSize);
      }
  }

在这里插入图片描述

  //判断左右孩子谁大,前提是必须有右孩子
  if(child+1<len && elem[child]<elem[child+1]){
      child++;//此时保存了最大值的下标
  }

在这里插入图片描述

    private void swap(int[] array,int i,int j){
        int tmp=array[i];
        array[i]=array[j];
        array[j]=tmp;
    }

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

import java.util.Arrays;

/**
 * @author Susie-Wen
 * @version 1.0
 * @description:
 * @date 2022/7/15 10:29
 */
public class TestHeap {
    public int[] elem;//堆的底层是数组
    public int usedSize;//当前堆中有效元素的数据个数

    //提供一个构造方法
    public TestHeap(){
        this.elem=new int[10];
        this.usedSize=0;
    }

    public void initArray(int[] array){
        elem= Arrays.copyOf(array,array.length);
        usedSize= elem.length;
    }

    //建堆:大根堆
    public void createHeap(){
        for(int parent=(usedSize-1-1)/2;parent>=0;parent--){
            shiftDown(parent,usedSize);
        }
    }

    /**
     *实现向下调整
     * @param parent 每棵子树的根节点下标
     * @param len 每棵子树的结束位置
     */
    private void shiftDown(int parent,int len){
        int child=2*parent+1;//使用公式
        //起码得有左孩子才能进入循环
        while(child<len){
            //判断左右孩子谁大,前提是必须有右孩子
            if(child+1<len && elem[child]<elem[child+1]){
                child++;//此时保存了最大值的下标
            }
            if(elem[child]>elem[parent]){
                swap(elem,child,parent);
                parent=child;
                child=2*parent-1;
            }else{
                break;
            }
        }
    }

    private void swap(int[] array,int i,int j){
        int tmp=array[i];
        array[i]=array[j];
        array[j]=tmp;
    }
}
public class Test{
    public static void main(String[] args) {
        TestHeap testHeap=new TestHeap();
        int[] array={27,15,19,18,28,34,65,49,25,37};
        testHeap.initArray(array);
        testHeap.createHeap();
        System.out.println("nlnancaklc");//在此处设置断点
    }
}

2.3.3 建堆时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是 近似值,多几个节点不影响最终结果):

  • 需要移动的节点的次数就是时间复杂度。

在这里插入图片描述

因此: 建堆的时间复杂度为O(N)。

题目:

在这里插入图片描述

在这里插入图片描述

2.4 堆的插入和删除

2.4.1堆插入

插入操作需要执行向上调整算法。

例如:向大根堆当中插入一个数字80,插入数据之后,必须保证仍然是一个大根堆。

在这里插入图片描述

    public void offer(int x){
        if(isFull()){
            //如果数组满了,就进行扩容
            elem=Arrays.copyOf(elem,elem.length*2);
        }
        this.elem[usedSize]=x;//将要插入的元素放到最末尾去
        usedSize++;
        shiftUp(usedSize-1);
    }

    //插入元素:向上调整
    public void shiftUp(int child){
        int parent=(child-1)/2;
        while(child>0){
            if(elem[child]>elem[parent]){
                swap(elem,child,parent);
                child=parent;
                parent=(child-1)/2;
            }else{
                break;
            }
        }
    }

    public boolean isFull(){
        return usedSize==elem.length;
    }

在这里插入图片描述

2.4.2堆删除

堆删除:删的是堆顶元素,常见操作是将堆顶元素与堆中最后一个元素交换,然后堆中元素个数减少一个,重新将堆顶元素往下调整。

在这里插入图片描述

    //出队:删除元素
    public int poll(){
        if(isEmpty()){
            return -1;//这里写一个异常也可以
        }
        int old=elem[0];//记录下来要删的值
        swap(elem,0,usedSize-1);
        usedSize--;
        shiftDown(0,usedSize);
        return old;
    }

    public boolean isEmpty(){
        return usedSize==0;
    }

3.常用接口介绍

3.1 PriorityQueue的特性

JAVA当中的PriorityQueue底层默认是一个小根堆

public class Test02 {
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue=new PriorityQueue<>();
        priorityQueue.offer(12);
        priorityQueue.offer(5);
        priorityQueue.offer(42);
        priorityQueue.offer(8);
        System.out.println(priorityQueue.peek());
        System.out.println(priorityQueue.poll());
    }
}

在这里插入图片描述

Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列, PriorityQueue是线程不安全的, PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。

在这里插入图片描述

3.2 PriorityQueue使用的注意事项

  1. 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
  1. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常。

比如:里面的元素放的是学生类

在这里插入图片描述
在这里插入图片描述

class Student implements Comparable<Student>{
    public int age;
    @Override
    public int compareTo(Student o) {
        return this.age-o.age;
    }
}
public class Test03 {
    public static void main(String[] args) {
        PriorityQueue<Student> priorityQueue=new PriorityQueue<>();
        priorityQueue.offer(new Student());
        priorityQueue.offer(new Student());
    }
}
  1. 不能插入null对象,否则会抛出NullPointerException。

在这里插入图片描述

  1. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容。

在这里插入图片描述


在这里插入图片描述

  1. 插入和删除元素的时间复杂度为
    在这里插入图片描述
  2. PriorityQueue底层使用了堆数据结构(注意:是小根堆)。
  3. PriorityQueue默认情况下是小堆————即每次获取到的元素都是最小的元素。

3.3 PriorityQueue的构造方法

此处只是列出了PriorityQueue中常见的几种构造方式:

构造器功能介绍
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(intinitialCapacity)创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异 常
PriorityQueue(Collection<? extends E> c)用一个集合来创建优先级队列

在这里插入图片描述


在这里插入图片描述

class Student implements Comparable<Student>{
    public int age;
    @Override
    public int compareTo(Student o) {
        return this.age-o.age;
    }
}
class AgeComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age-o2.age;
    }
}
public class Test03 {
    public static void main(String[] args) {
        AgeComparator ageComparator=new AgeComparator();
        PriorityQueue<Student> priorityQueue=new PriorityQueue<>(ageComparator);
        priorityQueue.offer(new Student());
        priorityQueue.offer(new Student());
    }
}

3.4 PriorityQueue的offer方法

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

3.5 PriorityQueue函数介绍

函数名功能介绍
boolean offer(E e)插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 O(log2N),注意:空间不够时候会进行扩容
E peek()获取优先级最高的元素,如果优先级队列为空,返回null
E poll()移除优先级最高的元素并返回,如果优先级队列为空,返回null
int size()获取有效元素的个数
void clear()清空
boolean isEmpty()检测优先级队列是否为空,空返回true

3.6 PriorityQueue扩容

以下是JDK 1.8中, PriorityQueue的扩容方式:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
	int oldCapacity = queue.length;
	// Double size if small; else grow by 50%
	int newCapacity = oldCapacity + ((oldCapacity < 64) ?
	(oldCapacity + 2) :
	(oldCapacity >> 1));
	// overflow-conscious code
	if (newCapacity - MAX_ARRAY_SIZE > 0)
	newCapacity = hugeCapacity(minCapacity);
	queue = Arrays.copyOf(queue, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
	if (minCapacity < 0) // overflow
	throw new OutOfMemoryError();
	return (minCapacity > MAX_ARRAY_SIZE) ?
	Integer.MAX_VALUE :
	MAX_ARRAY_SIZE;
}

在这里插入图片描述


在这里插入图片描述

优先级队列的扩容说明:

  • 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
  • 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
  • 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容

4.堆的应用

4.1 常见习题

  1. 下列关键字序列为堆的是:()
    A: 100,60,70,50,32,65
    B: 60,70,65,50,32,100
    C: 65,100,70,32,50,60
    D: 70,65,100,32,50,60
    E: 32,50,100,70,65,60
    F: 50,100,70,65,60,32
    答案:A

  2. 已知小根堆为8,15,10,21,34,16,12 ,删除关键字8之后需重建堆 ,在此过程中 ,关键字之间的比较次数是()
    A: 1
    B: 2
    C: 3
    D: 4
    答案:C

在这里插入图片描述

  1. 一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为()
    A: (11 5 7 2 3 17)
    B: (11 5 7 2 17 3)
    C: (17 11 7 2 3 5)
    D: (17 11 7 5 3 2)
    E: (17 7 11 3 5 2)
    F: (17 7 11 3 2 5)
    答案:C

  2. 最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后 ,其结果是()
    A: [3 ,2 ,5 ,7 ,4 ,6 ,8]
    B: [2 ,3 ,5 ,7 ,4 ,6 ,8]
    C: [2 ,3 ,4 ,5 ,7 ,8 ,6]
    D: [2 ,3 ,4 ,5 ,6 ,7 ,8]
    答案:C

4.2 top-k问题

top-k问题:求最大或者最小的前k个数据。比如:世界前500强公司。

对于这类问题,可以使用优先级队列。而优先级队列可以做是因为它的底层是堆,因此就相当于堆问题的应用。

top-k问题》》》》》》》》》》》》》》》

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] ret=new int[k];//这里创建的是k个数,不能是其他的整数
        if(k==0)return ret; //这里由测试用例可知,不能直接返回null 
        //由于是求最小的k个元素,因此创建大小为k的大根堆                        
        PriorityQueue<Integer> maxPQ=new PriorityQueue<>(k,new Comparator<Integer>() {
            @Override   //匿名内部类,重写comparator方法
            public int compare(Integer o1, Integer o2) {
            //这里的类型要写Integer,不能写int
                return o2-o1;//这里可以看出是创建大根堆,如果变成小根堆则是o1-o2
            }
        });
        for(int i=0;i<arr.length;i++){
            if(maxPQ.size()<k){
                maxPQ.offer(arr[i]);//只要堆没有满就入堆
            }else{
                //获取堆顶元素
                int top=maxPQ.peek();
                //找前k个最小的
                if(arr[i]<top){
                    maxPQ.poll();//出堆顶元素
                    maxPQ.offer(arr[i]);//把当前循环到的元素放到堆中
          //每次执行一次offer方法,就会重新调整一下堆,使得其堆顶元素一直是最大的
                }
            }
        }
        for(int i=0;i<k;i++){
            int val=maxPQ.poll();//每次弹出一个元素
            ret[i]=val;
        }
        return ret;
    }
}

在这里插入图片描述


在这里插入图片描述
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都 不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  • 1、用数据集合中前K个元素来建堆
    • 前k个最大的元素,则建小堆
    • 前k个最小的元素,则建大堆
  • 2、用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

4.3 堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

  • 1、建堆
    • 升序:建大堆
    • 降序:建小堆
  • 2、利用堆删除思想来进行排序
    建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

在这里插入图片描述

    public void heapSort(){
        int end=usedSize-1;
        while(end>0){
            swap(elem,0,end);shiftDown(0,end);
            end--;
        }
    }
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

温欣2030

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值