【数据结构】优先级队列(堆)

文章目录

  • 💐1. 优先级队列
    • 1.1 概念
  • 💐2.堆的概念及存储方式
    • 2.1 什么是堆
    • 2.2 为什么要用完全二叉树描述堆呢?
    • 2.3 为什么说堆是在完全二叉树的基础上进行的调整?
    • 2.4 使用数组还原完全二叉树
  • 💐3. 堆的常用操作-模拟实现
    • 3.1 堆的创建
      • 3.1.1 堆的向下调整(大根堆为例)
      • 3.1.2 建堆的时间复杂度
    • 3.2 堆的插入和删除
      • 3.2.1 堆的插入
      • 3.2.2 堆的删除
  • 💐4. PriorityQueue常用接口及特性
      • PriorityQueue的构造
      • 优先级队列的扩容源码分析
  • 💐5. PriorityQueue的比较方式
  • 💐6. 例子:使用优先级队列解决TOP-k问题

💐1. 优先级队列

1.1 概念

队列是一种先进先出的数据结构,但是呢,有时候,数据之间也会有优先级,就比如说,我们想要拿到一个集合中的前k大的数据,或者是在平时,我们在打游戏时,这时候来电话了,但是,游戏也不能让他退出呀,这时候就会忽略掉电话,由此可见,游戏的优先级要比电话的优先级更高;

💐2.堆的概念及存储方式

优先级队列是由堆实现的而堆实际实际上是在完全二叉树的基础上进行了调整,而此时也就用到了 二叉树的顺序存储方式 这句话是什么意思呢?请看下图:

2.1 什么是堆

这里不做详细的介绍,只需知道,所有引用类型所创建的对象都保存在堆上,包括数组;而这里的堆是将所有的元素,按照完全二叉树的顺序存储方式存储到了一维数组中;

在这里插入图片描述

2.2 为什么要用完全二叉树描述堆呢?

在这里插入图片描述

2.3 为什么说堆是在完全二叉树的基础上进行的调整?

堆又分为了大根堆和小根堆

在这里插入图片描述

​ 从上图就可以发现两个性质:

1.大根堆中,所有的父节点都比子节点大

2.小根堆中,所有的父节点都比子节点小

2.4 使用数组还原完全二叉树

​ 在二叉树文章中,提到过这样一条性质:如果每一个节点都有一个编号 i 的话,那么:

1.当 i > 0 时,i 节点的父亲节点为: (i - 1) / 2;

2.如果 2i +1 < 节点的总数时,则下标为 i 节点的左孩子节点的下标为 2i + 1

3.如果 2i + 2 < 节点的总数时, 则下标为 i 的节点的左孩子节点的下标为 2i + 1

因为是在数组中进行存储的,所以完全二叉树中每一个节点的编号都是数组中每一个元素的下标;所以这样就可以使用数组来还原完全二叉树,然后再对完全二叉树进行调整;

在这里插入图片描述

在上一篇文章中,对于 优先级队列的概念及存储方式进行了一个详细的讲解,那么,本篇文章主要是针对优先级队列底层实现大致是什么样子的,亲手写一下代码结合图为大家讲解:

💐3. 堆的常用操作-模拟实现

3.1 堆的创建

我们知道,在上一篇文章中讲过,所有的元素都是在数组中存储的,那么,如何利用数组来实现 大根堆小根堆的创建呢?就要考虑下面这个问题:

如果出现子节点比父节点大或者小该怎么办呢,怎么去调整呢?

3.1.1 堆的向下调整(大根堆为例)

在这里插入图片描述

可以看出,最后一棵子树的子节点比父节点大,我们所用的方法就是:

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

代码实现:

public class MyPriorityQueue {//底层数组private int[] element;//数组中的元素private int usedSize;//初始默认容量private static final int default_capacity = 9;public MyPriorityQueue(int[] arr) {this.element = new int[default_capacity];//传入一个数组,对element进行构造for(int i = 0; i<arr.length; i++) {element[i] = arr[i];usedSize++;}}//建一个大根堆public void buildHeap() {//parent 求出最后一棵子树的父亲节点for(int parent = (usedSize-2)/2; parent >= 0; parent--) {/*为什么减2而不是减1呢?因为,如果是最后一个节点的下标值,就是减一,但是,数组的长度值比下标大1,所以减2*///向下调整shiftDown(parent, usedSize);}}private void shiftDown(int parent, int len) {//求左孩子节点int child = (2*parent)+1;//判断是否有右孩子节点并且判断左孩子节点是否大于右孩子节点if(child+1 < len && element[child] < element[child+1]){//得到最大的孩子节点child++;}//判断孩子节点是否比父亲节点大if(element[child] > element[parent]) {//进行交换swap(parent, child);}}private void swap(int parent, int child) {int tmp = element[parent];element[parent] = element[child];element[child] = tmp;}public static void main(String[] args) {//测试用例int[] ele = {50, 45, 40, 20, 25, 35, 30, 10, 60};MyPriorityQueue myPriorityQueue = new MyPriorityQueue(ele);myPriorityQueue.buildHeap();}

但是,上述代码存在一个致命的问题:

在这里插入图片描述

代码优化:

    private void shiftDown(int parent, int len) {//求左孩子节点int child = (2*parent)+1;while(child < len) {//判断是否有右孩子节点并且判断左孩子节点是否大于右孩子节点if(child+1 < len && element[child] < element[child+1]){//得到最大的孩子节点child++;}//判断孩子节点是否比父亲节点大if(element[child] > element[parent]) {//进行交换swap(parent, child);//保证交换后,该树的子树仍然是大根堆parent = child;child = (2*parent)+1;}else {//表示以该节点为根的树已经是大根堆了break;}}}

在这里插入图片描述

3.1.2 建堆的时间复杂度

在学会了建堆以后,接下来就聊一聊建堆所用的复杂度吧!先说结论,最坏的情况是O(n),下面我来推到以下:

在推导之前先说明一下:因为堆是完全二叉树,而满二叉树也是完全二叉树,多几个节点也无所谓,时间复杂度本来就是一个近似值,所以为了容易理解,这里会用满二叉树进行推导

在这里插入图片描述

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

3.2 堆的插入和删除

3.2.1 堆的插入

堆的插入分为两个步骤:

1.将要添加的元素放在底层数组的最后一个位置;注意是否要扩容问题

2.向上调整该元素

下面讲解一下什么是向上调整

直接拿要添加的元素与根节点相比较,因为,本身已经是大根堆了,直接与根节点比较,符合条件就交换,不需要再与其他的节点比较

在这里插入图片描述

代码实现:

    //插入方法public void offer(int val) {//判断是否需要扩容if(is_full()) {this.element = Arrays.copyOf(element, element.length+1);}//向上调整this.element[usedSize] = val;shiftUp();usedSize++;}//向上调整private void shiftUp() {int parent = (usedSize-1)/2;int child = usedSize;while(child > 0) {if(element[parent] < element[child]) {swap(parent, child);}child = parent;parent = (child-1)/2;}}private boolean is_full() {return usedSize == element.length;}

3.2.2 堆的删除

1.将第一个元素和最后一个元素进行交换

2.节点实际上并没有被删除,而是节点的个数useSize减1

3.最后向上调整

在这里插入图片描述

代码实现

    //删除public void poll() {//排除空堆情况if(usedSize == 0) {return;}//交换第一个和最后一个元素swap(0,usedSize-1);//节点个数减1,不会对最后一个元素进行判断usedSize--;for(int parent = (usedSize-1)/2; parent >= 0; parent--) {//向下调整shiftDown(parent, usedSize);}}private void shiftDown(int parent, int len) {//求左孩子节点int child = (2*parent)+1;while(child < len) {//判断是否有右孩子节点并且判断左孩子节点是否大于右孩子节点if(child+1 < len && element[child] < element[child+1]){//得到最大的孩子节点child++;}//判断孩子节点是否比父亲节点大if(element[child] > element[parent]) {//进行交换swap(parent, child);//保证交换后,该树的子树仍然是大根堆parent = child;child = (2*parent)+1;}else {//表示以该节点为根的树已经是大根堆了break;}}}

💐4. PriorityQueue常用接口及特性

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

PriorityQueue的构造

构造器功能讲解
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity)创建一个初始容量为intialCapacity的优先级队列,注意:initialCapacity不能小于1, 否则会抛出IllegalArgumentException异常
PriorityQueue(Collection<? extends E> c)用一个集合来创建优先级队列
PriorityQueue(Comparator<? super E> comparator)自定义类型进行比较,传入一个比较器

代码实现

   public static void main(String[] args) {//创建一个空的优先队列,底层默认容量是11PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>();//创建一个空的优先级队列,指定容量为100PriorityQueue<Integer> priorityQueue2 = new PriorityQueue<>(100);List<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);//使用其他集合构造优先级队列PriorityQueue<Integer> priorityQueue3 = new PriorityQueue<>(list);//默认是小根堆,想要变成大根堆需要传入比较器PriorityQueue<Integer> priorityQueue4 = new PriorityQueue<>(new com());priorityQueue4.offer(1);priorityQueue4.offer(2);priorityQueue4.offer(3);}//定义一个比较器
class com implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o2-o1;}
}

PriorityQueue使用时需要注意:

1.使用时必须导入PriorityQueue所在的包,即:

import java.until.PriorityQueue;
  1. PriorityQueue中放置的元素必须要能够比较大小,不能插入不能比较大小的队象,否则会抛出ClassCastException异常
  2. 不能插入 null对象, 否则会抛出NullPointerException
  3. 没有容量限制,可以插入多个元素,其内部可以自动扩容
  4. 插入和删除元素的时间复杂度为O(log2N)
  5. PriorityQueue底层使用了堆数据结构
  6. PriorityQueue默认情况下是小根堆

Java中提供的方法:

函数功能介绍
boolean offer(E e)插入元素e, 插入成功返回true,如果e为空,抛出异常,空间不够时会进行扩容
E peek()获取优先级最高的元素,如果优先级队列为空,放回null
E pool()移除优先级最高的元素,如果优先级队列为空,返回null
int size()获取有效元素个数
void clear()清空
boolean isEmpty()检测优先级队列是否为空,如果为空返回true

优先级队列的扩容源码分析

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

💐5. PriorityQueue的比较方式

PriorityQueue底层使用堆结构,所以,内部的元素必须能够比较大小,PriorityQueue采用了:Comparble 和 Comjparator两种方式。

  1. Comparble默认的内部比较方式,如果用户插入自定义类型对象是,该类对象必须要实现Comparble接口,并且要重写compareTo方法

  2. 也可以使用比较器,用户自己实现一个比较器类且实现Comaparator接口,并且让该类重写compare方法,指定根据对象的什么内容进行比较,然后再实例化PriorityQueue对象时,把比较器传进去;

源码讲解:

	//内部定义的比较器对象private final Comparator<? super E> comparator;//如果用户没有提供比较器,则使用内部的比较方式,将comparator置为nullpublic PriorityQueue() {this(DEFAULT_INITIAL_CAPACITY, null);}//如果用户提供了比较器,则采用提供的比较器进行比较public PriorityQueue(int initialCapacity,Comparator<? super E> comparator) {// Note: This restriction of at least one is not actually needed,// but continues for 1.5 compatibilityif (initialCapacity < 1)throw new IllegalArgumentException();this.queue = new Object[initialCapacity];this.comparator = comparator;}//在添加对象时进行向上调整//如果没有提供比较器,则采用内部比较方式,即Comparable//如果提供了比较器,则采用比较器进行比较private void siftUp(int k, E x) {if (comparator != null)siftUpUsingComparator(k, x);elsesiftUpComparable(k, x);}//采用comparable进行比较private void siftUpComparable(int k, E x) {Comparable<? super E> key = (Comparable<? super E>) x;while (k > 0) {int parent = (k - 1) >>> 1;Object e = queue[parent];if (key.compareTo((E) e) >= 0)break;queue[k] = e;k = parent;}queue[k] = key;}//采用comparator进行比较private void siftUpUsingComparator(int k, E x) {while (k > 0) {int parent = (k - 1) >>> 1;Object e = queue[parent];if (comparator.compare(x, (E) e) >= 0)break;queue[k] = e;k = parent;}queue[k] = x;}

在这里插入图片描述

在这里插入图片描述

💐6. 例子:使用优先级队列解决TOP-k问题

TOP-K 问题:求数据集合中前K个最大的元素或者最小的元素,一般数据量都比较大;

[面试题 17.14. 最小K个数 - 力扣(LeetCode)]()

class Solution {public int[] smallestK(int[] arr, int k) {//创建一个优先级队列PriorityQueue<Integer> heap = new PriorityQueue<>();//将所有元素加入到队列中for(int i = 0; i<arr.length; i++) {heap.add(arr[i]);}//创建一个数组用来存储前k个元素int[] ans = new int[k];//将堆中的前k个元素保存在数组中for(int i = 0; i<k; i++) {ans[i] = heap.poll();}//返回数组return ans;}
}

但是,上面这个代码只能针对于数据量较小时才行,如果数据量太大就会造成时间复杂度过高,比如,有一亿个数据,如果使用以上代码的话,会先将一亿个数据都保存在堆中,然后进行比较,这就得不偿失了;那么就要对代码进行一个优化:

1.用集合中的前k个元素建堆;

  • 前k大个元素建小堆
  • 前k小个元素建大堆

2.用剩余的n(元素的个数) - k个元素与堆中的元素进行比较;最后堆中剩余的元素就是前k个最大或最小的元素

代码实现:

    public int[] smallestK(int[] arr, int k) {//求前k个最小元素,所以要建大根堆,因为PriorityQueue默认的是小根堆,所以要传入比较器PriorityQueue<Integer> heap = new PriorityQueue<>(new Com());//将前k个元素加入到堆中for(int i = 0; i<k; i++){heap.add(arr[i]);}if(heap.isEmpty()) {return new int[]{};}//用剩余的元素与堆顶元素比较for(int i = k; i<arr.length; i++) {int x = heap.peek();//这里的条件语句求的是前k个最小元素if(arr[i] < x){heap.poll();heap.offer(arr[i]);}}int[] ans = new int[k];for(int i = 0; i<k; i++){ans[i] = heap.poll();}return ans;}
}
//自定义一个比较器
class Com implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o2 - o1;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/86437.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV实现的F矩阵+RANSAC原理与实践

1 RANSAC 筛选 1.1 大致原理 Random sample consensus (RANSAC)&#xff0c;即随机抽样一致性&#xff0c;其是一种用于估计模型参数的迭代方法&#xff0c;特别适用于处理包含离群点&#xff08;outliers&#xff09;的数据集 RANSAC 的主要思想是随机采样数据点&#xff0…

ExcelServer EXCEL服务器使用- 用户、角色权限配置

Excel文件服务器搭建 搭建Excel服务器 1、登录 默认 用户名 Admin 密码 3 2、角色管理 添加修改角色 角色配置在 系统管理->角色.fexm文件夹下 可以像修改excel文件一样 修改角色 3、用户管理 添加修改用户 用户的修改在 系统管理->用户.fexm 可以像excel一样编辑用户…

链式二叉树的实现及遍历(C语言版)

目录 1 基本概念 1.1 树的概念 1.2 二叉树的链式表示 1.2.1 "左孩子右兄弟"表示法 1.2.2 "左右子树"表示法 1.2.3 手动构建一棵树 2 树的遍历 2.1 前序遍历/先序遍历 2.2 中序遍历 2.3 后序遍历 2.4 层序遍历 2.4.1 算法思想 ​编辑 2.4.2 带头…

MongoDB基础详解

一、MongoDB概述 MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统&#xff0c;由 C 编写的。MongoDB 提供了 面向文档 的存储方式&#xff0c;操作起来比较简单和容易&#xff0c;支持“无模式”的数据建模&#xff0c;可以存储比较复杂的数据类型&#xff0c;是一…

【Linux学习】02Linux基础命令

Linux&#xff08;B站黑马&#xff09;学习笔记 01Linux初识与安装 02Linux基础命令 文章目录 Linux&#xff08;B站黑马&#xff09;学习笔记前言02Linux基础命令Linux的目录结构Linux命令入门ls命令 目录切换相关命令(cd/pwd)cd命令pwd命令 相对路径、绝对路径和特殊路径符创…

Seata流程源码梳理下篇-TC

我们上篇简单梳理了下TM、RM的一些流程&#xff08;离现在过得挺久的了&#xff0c;这篇我们这篇来梳理下TC的内容。 TC (Transaction Coordinator) - 事务协调者 维护全局和分支事务的状态&#xff0c;驱动全局事务提交或回滚。 TM (Transaction Manager) - 事务管理器 定…

将本地项目上传至Github详解

目录 1 前言2 本地代码上传2.1 命令行方法2.2 图形界面法2.3 结果 1 前言 GitHub是一个面向开源及私有软件项目的托管平台&#xff0c;因为只支持Git作为唯一的版本库格式进行托管&#xff0c;故名GitHub 。开发者常常将github作为代码管理平台&#xff0c;方便代码存储、版本…

基于SpringBoot的的师生健康信息管理系统

目录 前言 一、技术栈 二、系统功能介绍 管理员功能模块 学生功能模块 教师功能模块 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着移动应用技术的发展&#xff0c;越来越多的用户借助于移动手机、电脑完成生活中的事务&#xff0c;许多的传统行业也…

超级详细 SQL 优化大全

1、MySQL的基本架构 1&#xff09;MySQL的基础架构图 左边的client可以看成是客户端&#xff0c;客户端有很多&#xff0c;像我们经常你使用的CMD黑窗口&#xff0c;像我们经常用于学习的WorkBench&#xff0c;像企业经常使用的Navicat工具&#xff0c;它们都是一个客户端。右…

北工大汇编题——分支程序设计

题目要求 信息检素程序设计&#xff1a;在数据区&#xff0c;有9个不同的信息&#xff0c;编号 0-8&#xff0c;每个信息包括20 个字符。从键盘接收 0-8 之间的一个编号&#xff0c;然后再屏幕上显示出相应编号的信息内容&#xff0c;按“q”键退出 完整代码 DATAS SEGMENTn0…

Day 03 python学习笔记

位运算 基于二进制的运算&#xff08;计算机的底层基于位运算&#xff09; 计算机最小单位&#xff1a;bit (比特/位/二进制) 1byte&#xff08;字节&#xff09; 8bit &#xff08; 0000 0000&#xff09; &&#xff1a;与 &#xff08;全真为真&#xff0c;一假则…

项目开发过程中遇到了什么困难?

1.需求最初是什么样的&#xff1f; 2.如何挖掘的需求&#xff0c;挖掘后真实的需求是什么样的&#xff1f; 3.我们做了那些调查&#xff1f; 4.我们给出了那些方案&#xff0c;优缺点是什么&#xff1f; 5.根据实际情况&#xff0c;老板的期望&#xff0c;最终我们选择的什…

Leetcode 01-算法入门与数组-③数组排序

LeetCode 01-算法入门与数组-③数组排序 一. 冒泡排序 1. 冒泡排序算法思想 冒泡排序&#xff08;Bubble Sort&#xff09;基本思想&#xff1a; 经过多次迭代&#xff0c;通过相邻元素之间的比较与交换&#xff0c;使值较小的元素逐步从后面移到前面&#xff0c;值较大的元素…

SAP PO运维(一):系统概览异常处理

打开SAP PIPO Netweaver Administration界面,系统概览下显示异常: 参考SAP note: 2577844 - AS Java Monitoring and Logging parametrization best practice service/protectedwebmethods = SDEFAULT -GetVersionInfo -GetAccessPointList -ListLogFiles -ReadLogFile -Para…

为什么选择Spring Cloud

Spring Cloud与Netflix Netflix是一家做视频网站的公司&#xff0c;之所以要说一下这个公司是因为Spring Cloud在发展之初&#xff0c;Netflix做了很大的贡献。包括服务注册中心Eureka、服务调用Ribbon、Feign&#xff0c;服务容错限流Hystrix、服务网关Zuul等众多组件都是Net…

Linux下ThinkPHP5实现定时器任务 - 结合crontab

实例一&#xff1a; 1.在/application/command创建要配置的PHP类文件&#xff0c;需要继承Command类&#xff0c;并重写configure和execute两个方法&#xff0c;例如: <?php namespace app\command; use think\console\Command; use think\console\Input; use think\cons…

FatFS文件系统在MCU上的应用

FatFS文件系统是单片机领域有名的一个文件系统&#xff0c;由于它的轻量级和兼容性&#xff0c;备受MCU开发者青睐。 在实现如U盘文件读写&#xff0c;SD卡的文件读写等工作时&#xff0c;我们往往需要一个文件系统来支持我们的工作。特别在一些MCU应用中&#xff0c;文件系统…

PPPoE配置

实验需求 配置IP地址使用PPPOE拨号上网设置路由让直播业务部和营销部都可以访问外网 实验拓扑 实验步骤 配置 R1地址池 电信链路&#xff1a; [Huawei]undo info-center enable Info: Information center is disabled. [Huawei]sysname r1 [r1]ip pool zhibo  //配置…

【沐风老师】3DMAX翻转折叠动画插件FoldFx使用方法详解

3DMAX翻转折叠动画插件FoldFx使用方法详解 3DMAX翻转折叠动画插件FoldFx,是3dMax运动图形工具,用于创建多边形折叠动画。用户几乎有无限的可能性,因为动画的每个方面都是可控的。 【适用版本】 适用于3dMax版本:2010及更新版本(推荐3dMax2016及更高版本)。 【安装方法】…

Go 围炉札记

文章目录 一、Go 安装 一、Go 安装 VScode下配置Go语言开发环境【2023最新】 基础篇&#xff1a;新手使用vs code新建go项目 vscode里安装Go插件和配置Go环境 Documentation Golang 配置代理 Go命令详解 一文详解Go语言常用命令 Go 语言教程 熬夜整理&#xff0c;最全的Go语…