优先级队列(堆)

本文介绍了堆的概念,作为优先级队列的一种实现,堆在操作系统任务调度和医疗资源分配等场景中有广泛应用。堆的时间复杂度分析显示,入队和出队操作均为O(logn)。二叉堆作为最常见的堆类型,通常以完全二叉树形式存储于数组中。最大堆和最小堆分别用于解决TopK最大值和最小值问题。文章还详细阐述了最大堆的添加元素、删除最大值以及原地堆化的操作,并提供了Java实现的最大堆代码示例。
摘要由CSDN通过智能技术生成

堆的概念

按照优先级的大小动态出队(动态指的是元素个数的动态变化,而非固定),对比普通的队列是先入先出

现实中的堆

  • 1操作系统中的任务调度,(一般内核任务的优先级是高于普通应用的)
  • 2比如医院中,医生要按照病人的病情来动态安排手术,比如今天有三个病人,已经安排好明天手术的顺序,突然来了一个病重的病人,那么医生就得先给这个病人做手术

优先级队列和普通队列的时间复杂度比较

入队时间复杂度出队(最大值)时间复杂度
普通队列O(1)O(n)
优先级队列O(logn(O(logn)
  • 在计算机领域,遇到logn级别的时间复杂度,几乎都是跟树有关(不一定非要构造树结构,但是在逻辑上都运用了树的思想)

堆的实现

基于二叉树的堆叫做二叉堆,也有d叉堆和索引堆

二叉堆的特点

二叉树的存储

是一颗完全二叉树,基于数组存储(因为完全二叉树的元素都是靠左排列,数组存储的时候不会浪费空间,其他二叉树的存储需要用链式结构,因为用数组会导致大量浪费)

两种特别的堆

  • 如果堆中根节点>=子树节点中的值——>最大堆,大根堆(所有的子树也满足这个要求)
  • 如果堆中根节点<=子树节点中的值——>最小堆,小根堆(所有的子树也满足这个要求)
  • 这两个堆可以完成TopK问题,如果求前k个最大值,就用最小堆,如果求前k个最小值,就用最大堆

节点的层次和节点的大小没有任何关系,只能保证在当前树中,树根是最大值

堆的编号问题

因为堆是基于数组存储的,所以节点之间的关系要通过数组下标来表示,且从0开始编号

假设子节点为i

  • 则父节点为(i-1)/2
  • 左子树的编号为2*i+1
  • 右子树的编号为2*i+2

基于动态数组实现的最大堆

因为JDK中实现的是最小堆,所以我们来实现一下最大堆

数据结构

public class MyHeap {
    private List<Integer> element;//用于存储堆的内容
    private int size=0;//用于记录堆中元素个数
}

两大核心操作

上浮操作

  •  比如我们要给这个最大堆加入一个元素,因为是基于数组实现的,一个元素的添加应该在size-1的位置,但是这个新加的元素可能会破坏这个最大堆的结构,比如我们添加了49,那么49比它的父亲节点33大,这是不符合最大堆的规定,所以我们引入上浮操作,
  • 一直将这个元素与父节点比较,如果小于父节点的值,就进行交换,直到它成为根节点,也就是这个堆中最大的数或者是当它的父节点大于这个值(符合最大堆的规定了)

下沉操作

  •  如果我们要取出这个最大堆的最大值,那么就是取出其根节点,但是这是基于数组实现的堆,我们要删除数组的第一个元素,代价比较大,因为会移动后面所有的元素,我们可以将根节点的值跟size-1的值进行交换,然后删除size-1的值,这样消耗比较小
  • 但是这样我们删除了一个元素,其堆的结构不满足最大堆的结构,所以我们引入了下沉操作,对根节点进行下沉操作
  • 每次将父节点与其左右子树的值进行比较,如果比其左右子树中最大的值小,就与之进行交换,直到这个节点不存在左右子树或者其左右子树的值都比父节点大
 public int pollMax(){
        //将最大元素出队 就是将根节点出队
        if (isEmpty()){
            throw new NoSuchElementException("heap is empty");
        }
        int max=element.get(0);
        element.set(0,element.get(size-1));
        element.remove(size-1);
        size--;
        siftdowm(0);
        return max;
}

 private void siftdowm(int k) {
        while (leftchild(k)<size){
            //说明还是存储子树的
            int j=leftchild(k);
            if (j+1<size&&element.get(j+1)>element.get(j)){
                j=j+1;
                //现在j表示的就是左右子树中值最大那个值
            }
            if (element.get(k)>=element.get(j)){
                break;
                //当前元素的值大于左右子树的值,下沉结束
            }else {
                swap(k,j);
                k=j;
            }
        }
    }
    private void swap(int k, int parent) {
        int child=element.get(k);
        int par=element.get(parent);
        element.set(parent,child);
        element.set(k,par);
    }

最大堆的两种建立

利用add方法一直添加元素

  public void add(int val){
        element.add(val);//在size的地方添加元素
        size++;
        siftup(size-1);//对新加入的元素进行上浮操作
    }
  private void siftup(int k) {
        while (k>0&&element.get(k)>element.get(parent(k))){
            swap(k,parent(k));
            k=parent(k);
        }
    }

    private void swap(int k, int parent) {
        int child=element.get(k);
        int par=element.get(parent);
        element.set(parent,child);
        element.set(k,par);
    }

原地堆化 heapify(把一个任意数组变成最大堆)

    public MyHeap(int[] arr){
        element=new ArrayList<>(arr.length);
        for (int i : arr) {
            element.add(i);
            size++;
        }//将所有元素放入存储数据的动态数组中
        for (int i = parent(size-1); i >=0 ; i--) {
            siftdowm(i);
        }//从第一个非叶子节点进行siftdown操作
        
    }
    private void siftdowm(int k) {
        while (leftchild(k)<size){
            //说明还是存储子树的
            int j=leftchild(k);
            if (j+1<size&&element.get(j+1)>element.get(j)){
                j=j+1;
                //现在j表示的就是左右子树中值最大那个值
            }
            if (element.get(k)>=element.get(j)){
                break;
                //当前元素的值大于左右子树的值,下沉结束
            }else {
                swap(k,j);
                k=j;
            }
        }
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

库里不会投三分

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

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

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

打赏作者

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

抵扣说明:

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

余额充值