java优先级队列(基于堆)

简介: java优先级队列(基于堆)

前言

博主个人社区: 开发与算法学习社区

博主个人主页:Killing Vibe的博客

欢迎大家加入,一起交流学习~~

好久没更新数据结构相关的文章了,之前还遗留了优先级队列的文章,现在补上~

一、优先级队列的应用

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

普通队列:FIFO按照元素的入队顺序出队,先入先出

现实生活中的优先级队列 PriorityQueue

1.1 医生根据病人排手术排期

排期时包括具体手术时,病人的人数都在动态变化

病情相同的情况下按照先来先排,若病情较重,优先安排手术。

举个栗子:

10.25 安排手术 10.26 手术 手术三人

此时若突然来了一个病情比较严重的病人,优先安排手术。

此时数据在动态变化

和排序的区别:

排序指的是当前集合的元素个数是确定的,不会发生变化。

1.2 操作系统的任务调度

系统的任务一般都比普通的应用要高

CPU、内存等资源是有限的,当资源不够用时,优先让优先级较高的应用获取资源

在这里插入图片描述

二、基于二叉树的堆(二叉堆)

2.1 二叉堆的特点

2.1.1 二叉堆的存储结构

二叉堆是一颗完全二叉树,基于数组存储(元素都是靠左排列,数组中存储时不会浪费空间)

只有完全二叉树适合使用数组这种结构来存储, 其他的二叉树都要用链式结构

在这里插入图片描述

2.1.2 关于节点值

堆中根节点值 >= 子树节点中的值(最大堆,大根堆)

堆中根节点值 <=子树中节点的值(最小堆,小根堆)

节点的层次和节点的大小没有任何关系,只能保证当前树中,树根是最大值。其他节点层次大小关系不确定。

堆中树根 >= 子树中所有节点,所有子树也仍然满足堆的定义。

在这里插入图片描述

注意:

  1. JDK中的PriorityQueue默认是基于最小堆的实现。
  2. 最大堆中“高”的结点未必就比 “低”的结点大(如上图)

2.1.3 二叉堆的父子结点编号

因为堆是基于数组来存储的,节点的之间的关系通过数组下标表示

从0开始编号,数组下标也是从0开始

假设此时结点编号为 i ,且存在父节点

  • 父节点编号 parent = (i -1)/2
  • 左子树的编号 left = 2 * i +1
  • 右子树的编号 right = 2 * i +2

在这里插入图片描述

2.2 二叉堆的基础表示

2.2.1 向堆中添加元素(shiftUp操作)

从尾部新增一个元素52,堆是数组实现的。

此时这个堆仍然满足完全二叉树的性质。

但是此时这个完全二叉树就不再是一个最大堆了,因此需要进行元素的上浮操作,让新添加的元素上浮到合适位置。

步骤如下:

1.尾部添加新元素

在这里插入图片描述

  1. 不断将此时索引 k 和父节点的索引 i 对应的元素进行大小关系比较,若大于父节点就交换彼此的节点值,直到当前节点 <= 父节点为止 or 走到树根。

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

最后52上浮到最大的位置

在这里插入图片描述

上浮操作的终止条件:

  1. 当前已经上浮到树根 =》 这个元素一定是最大值
  2. 当前元素 <= 父节点对应的元素值,此时元素落到在正确位置

2.2.2 在堆中取出最大值(shiftDown 操作)

在堆顶取得最大值后,如何移动元素可以保证此时还是个最大堆呢?

在这里插入图片描述

步骤如下:

  1. 最大堆的最大值一定处在树根节点,直接取出树根即可
  2. 需要融合左右两个子树,使得取出树根后这棵树仍然是最大堆
  3. 将堆中最后一个元素顶到堆顶,然后进行元素的下沉操作。 shifDown后 使其仍然满足最大堆的性质

在这里插入图片描述

其实堆排序就是建立最大堆,然后在最大堆的基础上进行调整操作,就可以得到一个升序集合~~

2.2.3 堆化(heapify)

现在给一个任意的整型数组 =》 都可以看作是一个完全二叉树,距离最大堆就差元素调整操作。

方法一:建堆

将这n个元素依次调用add方法添加到一个新的最大堆中,遍历原数组,创建一个新的最大堆,调用最大堆的add方法即可。

时间复杂度为:O($nlogn$)

空间复杂度为:O($n$)

方法二:原地heapify(重要)

从最后一个非叶子结点开始进行元素siftDown操作

从当前二叉树中最后一个小子树开始调整,不断向前,直到调整到根节点。

不断将子树调整为最大堆的时,最终走到树根时,左右子树已经全部是最大堆,只需最后下沉根节点就能的到最终的最大堆。

时间复杂度为 Sn = $nlog_2(n+1)$ ,因此最终可得渐进复杂度为O($n$)

在这里插入图片描述

三、代码实现

写一个基于动态数组实现最大堆的实例:

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

public class MaxHeap {
    // 实际存储元素的数组
    private List<Integer> elementData;
    // 当前堆中元素个数
    private int size;

    public MaxHeap() {
        this(10);
    }

    public MaxHeap(int size) {
        elementData = new ArrayList<>(size);
    }

    /**
     * 将任意的整型数组arr调整为堆
     * @param arr
     */
    public MaxHeap(int[] arr) {
        elementData = new ArrayList<>(arr.length);
        // 1.先将所有元素复制到data数组中
        for (int i : arr) {
            elementData.add(i);
            size ++;
        }
        // 2.从最后一个非叶子结点开始进行siftDown操作
        for (int i = parent(size - 1); i >= 0; i--) {
            siftDown(i);
        }
    }

    public void add(int val) {
        // 1.直接向数组末尾添加元素
        elementData.add(val);
        size ++;
        // 2.进行元素的上浮操作
        siftUp(size - 1);
    }

    /**
     * 取出当前最大堆的最大值
     * @return
     */
    public int extractMax() {
        if (size == 0) {
            throw new NoSuchElementException("heap is empty!cannot extract!");
        }
        // 树根就是最大值结点
        int max = elementData.get(0);
        // 将数组的末尾元素顶到堆顶
        elementData.set(0,elementData.get(size - 1));
        // 将数组的最后一个元素从堆中删除
        elementData.remove(size - 1);
        size --;
        // 进行元素的下沉操作,从索引为0开始
        siftDown(0);
        return max;
    }

    public int peekMax() {
        if (isEmpty()) {
            throw new NoSuchElementException("heap is empty!cannot peek!");
        }
        return elementData.get(0);
    }

    /**
     * 从索引k开始进行元素的下沉操作
     * @param k
     */
    private void siftDown(int k) {
        // 还存在子树
        while (leftChild(k) < size) {
            int j = leftChild(k);
            // 此时还存在右子树
            if (j + 1 < size && elementData.get(j + 1) > elementData.get(j)) {
                // 此时还存在右子树且右子树的值还比左子树大
                j ++;
            }
            // 索引j一定对应了左右子树的最大值索引
            if(elementData .get(k) >= elementData.get(j)) {
                // 当前元素 >= 左右子树最大值,下沉结束,元素k落在了最终位置
                break;
            }else {
                swap(k,j);
                k = j;
            }
        }
    }


    private void siftUp(int k) {
        while (k > 0 && elementData.get(k) > elementData.get(parent(k))) {
            swap(k,parent(k));
            k = parent(k);
        }
    }

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


    public int getSize() {
        return size;
    }

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

    /**
     * 找到结点k对应的父节点的索引
     * @param k
     * @return
     */
    private int parent(int k) {
        return (k - 1) >> 1;
    }

    /**
     * 找到结点k对应的左子树的索引
     * @param k
     * @return
     */
    private int leftChild(int k) {
        return (k << 1) + 1;
    }

    /**
     * 找到结点k对应的右子树的索引
     * @param k
     * @return
     */
    private int rightChild(int k) {
        return (k << 1) + 2;
    }

    @Override
    public String toString() {
        return elementData.toString();
    }
}

总结

基于堆的优先级队列可以用于解决Top K问题,博主推荐几道💪扣编程题:

  • 面试题17.14 最小K个数
  • 347.前k个高频元素
  • 373.查找最小的K对数字

后续博主会更新自己的题解以及思路,大家也可以自己做做,看看官方题解~

相关文章
|
3月前
|
Java
【Java基础面试三十二】、new String(“abc“) 是去了哪里,仅仅是在堆里面吗?
这篇文章解释了Java中使用`new String("abc")`时,JVM会将字符串直接量"abc"存入常量池,并在堆内存中创建一个新的String对象,该对象会指向常量池中的字符串直接量。
|
3月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
140 4
|
3月前
|
存储 算法 Java
解释 Java 堆空间和垃圾收集
【8月更文挑战第22天】
36 0
|
19天前
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
Java对象一定分配在堆上吗?
|
1月前
|
存储 算法 Java
🏗️Java零基础:深入了解Java 堆
【10月更文挑战第2天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
21 3
|
1月前
|
Java Linux 调度
Java线程的优先级详解
Java线程的优先级机制允许开发者根据程序需求为线程设定不同优先级,范围通常在1到10之间,默认优先级为5。高优先级线程在执行时通常会得到更多的CPU时间,但这并不意味着低优先级线程会被完全忽略。系统资源分配仍然取决于具体的调度策略。理解线程优先级有助于优化多线程应用的性能。
|
1月前
|
存储 安全 Java
【用Java学习数据结构系列】探索栈和队列的无尽秘密
【用Java学习数据结构系列】探索栈和队列的无尽秘密
30 2
|
2月前
|
Java API 容器
JAVA并发编程系列(10)Condition条件队列-并发协作者
本文通过一线大厂面试真题,模拟消费者-生产者的场景,通过简洁的代码演示,帮助读者快速理解并复用。文章还详细解释了Condition与Object.wait()、notify()的区别,并探讨了Condition的核心原理及其实现机制。
|
2月前
|
JSON 前端开发 JavaScript
java中post请求调用下载文件接口浏览器未弹窗而是返回一堆json,为啥
客户端调接口需要返回另存为弹窗,下载文件,但是遇到的问题是接口调用成功且不报错,浏览器F12查看居然返回一堆json,而没有另存为弹窗; > 正确的效果应该是:接口调用成功且浏览器F12不返回任何json,而是弹窗另存为窗口,直接保存文件即可。
141 2
|
1月前
|
存储 算法 Java
【用Java学习数据结构系列】用堆实现优先级队列
【用Java学习数据结构系列】用堆实现优先级队列
29 0