数据结构之优先级队列

系列文章目录

数据结构之ArrayList_arraylist o(1) o(n)-CSDN博客

数据结构之LinkedList-CSDN博客

数据结构之栈-CSDN博客

数据结构之队列-CSDN博客

数据结构之二叉树-CSDN博客


目录

系列文章目录

前言

一、优先级队列和堆

二、堆的模拟实现

1. 堆的创建

2. 计算建堆的时间复杂度

3. 堆的插入

4. 堆的删除

三、优先级队列源码

1. 构造方法

2. 扩容机制

3. 比较机制

四、topK问题


前言

本文主要介绍了优先级队列和堆的实现原理。首先讲解了堆的基本概念,包括大根堆和小根堆的存储结构和父子节点关系。然后详细讲解了堆的模拟实现过程,包括创建堆、插入元素和删除元素的操作步骤及时间复杂度分析。接着分析了Java中PriorityQueue的源码实现,包括构造方法、扩容机制和比较机制。最后讨论了topK问题的解决方案,提出使用大根堆/小根堆来高效获取前k个最小/最大元素的算法思路。文章通过代码示例展示了如何实现这些数据结构,并分析了相关操作的时间复杂度。


一、优先级队列和堆

优先级队列的两个基本操作:返回高优先级对象,添加新的对象;

PriorityQueue 底层使用了堆这种数据结构,堆是在完全二叉树的基础上进行了一些调整;

堆是按照完全二叉树的顺序存储(层序遍历)的方式存储;

小根堆:孩子节点都比根节点大;

大根堆:孩子节点都比根节点小;

如果根节点的下标是 i,左孩子节点的下标为 2 * i + 1,右孩子节点的下标为 2 * i + 2;

如果孩子节点的下标是 i,根节点的下标为 (i - 1)/ 2;

二、堆的模拟实现

1. 堆的创建

思路(以大根堆为例):

从后往前遍历每一棵子树,如果根节点的值比左右孩子的节点的值小,就将左右孩子节点的最大值和根节点的值交换;

交换完成后,这棵树的左右子树有可能出现孩子节点的值比根节点的值大的情况,因此需要将已经交换的孩子节点再次作为根节点,判断根节点和左右孩子节点的值,直至交换完最后一棵子树;

createHeap(): void 创建堆,从后往前遍历每一棵子树,向下调整根节点值;

shiftDown(int parent, int usedSize): void 计算孩子节点下标,如果孩子节点的值大于根节点的值,向下调整根节点的值;

public class Heap {private int[] elem;private int usedSize;public Heap(){this.elem = new int[10];}public Heap(int[] array){int n = array.length;this.elem = new int[n];for(int i = 0; i < array.length; i++){this.elem[i] = array[i];this.usedSize++;}}public void createHeap(){for(int parent = (this.usedSize - 1 - 1) / 2; parent >= 0; parent--){shiftDown(parent, this.usedSize);}}private  void shiftDown(int parent, int usedSize){int child = parent * 2 + 1;while(child < usedSize){if(child + 1 < usedSize && this.elem[child] < this.elem[child + 1]){child++;}if(this.elem[parent] < this.elem[child]){swap(parent, child);parent = child;child = parent * 2 + 1;}}}private void swap(int i, int j){int tmp = this.elem[i];this.elem[i] = this.elem[j];this.elem[j] = tmp;}
}

2. 计算建堆的时间复杂度

假设有 n 个节点,那么堆的高度为:h = log2^(n + 1);

计算节点的个数:从第一层到最后一层分别为 2^0, 2^1, 2^2,..., 2^(h-3), 2^(h-2), 2^(h-1);

计算要调整的高度:最坏情况下,从第一层到倒数第二层都需要调整,最后一层不需要调整,调整的高度分别是 h - 1, h - 2, h - 3, ..., 2, 1, 0;

因此需要花费的时间为:

T(n) = 2^0 * (h - 1) + 2^1 * (h - 2) + 2^2 * (h - 3) + 2^3 * (h - 4) + ... + 2^(h - 3) * 2 + 2^(h - 2) * 1;

2 * T(n) = 2^1 * (h - 1) + 2^2 * (h - 2) + 2^3 * (h - 3) + ... + 2^(h - 2) * 2 + 2^(h - 1) * 1;

使用错位相减计算:

T(n) = -2^0 * (h - 1) + 2^1 + 2^2 + 2^3 + ... + 2^(h - 3) + 2^(h - 2) + 2^(h - 1);

T(n) = 2^0 + 2^1 + 2^2 + 2^3 + ... + 2^(h - 3) + 2^(h - 2) + 2^(h - 1) - h;

使用等比数列求和公式:

T(n) = 1 * (1 - 2^h) / (1 - 2)  - h = 2^h - 1 - h;

将 h = log2^(n + 1) 带入公式:T(n) = n - log2^(n + 1);

当 n 足够的大情况下,log2^(n + 1)相比于 n 是可以忽略的,因此建堆的时间复杂度为 O(N);

3. 堆的插入

思路:

先在最后一个位置放上插入的元素,再将这个元素向上调整;

向上调整时,该节点作为 child 节点和根节点发生了交换,这个节点又会作为新的 child 节点,可能仍然需要继续向上调整,因此需要循环直至 child 作为整个完全二叉树的根节点;

如果没有发生交换,说明已经是一个大根堆了,则跳出循环即可;

offer(int val): void 向堆中添加元素;

shiftUp(int child): void 如果根节点的值比孩子节点的值小,向上调整孩子节点的值;

    public void offer(int val){if(isFull()){this.elem = Arrays.copyOf(this.elem,  2 * this.elem.length);}this.elem[this.usedSize] = val;this.usedSize++;shiftUp(this.usedSize - 1);}private boolean isFull(){return this.usedSize == this.elem.length;}private void shiftUp(int child){while(child != 0){int parent = (child - 1) / 2;if(this.elem[child] > this.elem[parent]){swap(parent, child);}else{break;}child = parent;}}

4. 堆的删除

思路:

将根节点和最后一个位置的元素进行交换,再将根节点位置的元素向下调整;

poll(): int 弹出堆顶元素;

    public int poll(){if(isEmpty()){return -1;}swap(0, this.usedSize - 1);this.usedSize--;shiftDown(0, this.usedSize);return this.elem[this.usedSize];}public boolean isEmpty(){return this.usedSize == 0;}

三、优先级队列源码

PriorityQueue 默认是小根堆;

PriorityQueue 中放置的元素必须能够比较大小,否则会抛出 ClassCastException 异常;

PriorityQueue 中不能放置 null 对象,否则会抛出空指针异常;

插入和删除元素的时间复杂度为 O(logN);

1. 构造方法

两个构造方法本质上都是调用 :

public PriorityQueue(int initialCapacity,Comparator<? super E> comparator)

第一个参数为初始化容量,第二个参数为比较器;

默认的初始化容量 DEFAULT_INITIAL_CAPCITY 为 11;

public class PriorityQueue<E> extends AbstractQueue<E>implements java.io.Serializable {private static final long serialVersionUID = -7720805057305804111L;private static final int DEFAULT_INITIAL_CAPACITY = 11;transient Object[] queue; // non-private to simplify nested class accessprivate int size = 0;private final Comparator<? super E> comparator;transient int modCount = 0; // non-private to simplify nested class accesspublic PriorityQueue() {this(DEFAULT_INITIAL_CAPACITY, null);}public PriorityQueue(int initialCapacity) {this(initialCapacity, null);}public PriorityQueue(Comparator<? super E> comparator) {this(DEFAULT_INITIAL_CAPACITY, comparator);}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;}
}

2. 扩容机制

MAX_ARRAY_SIZE 最大的数组容量为 Integer.MAX_VALUE - 8;

grow(int minCapacity): void 扩容:

当前的容量小于 64 字节:当前容量 + 当前容量 + 2 字节;

当前容量大于等于 64 字节:当前容量 * 1.5;

如果扩容后的容量大于 MAX_ARRAY_SIZE,调用 hugeCapacity(int minCapacity): int

hugeCapacity(int minCapacity): int 大容量扩容机制:

如果所需的最小容量溢出了(变成负数),则抛出异常;

如果所需的最小容量大于 MAX_ARRAY_SIZE,扩容为 Integer.MAX_VALUE

如果所需的最小容量小于等于 MAX_ARRAY_SIZE,扩容为  MAX_ARRAY_SIZE

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;/*** Increases the capacity of the array.** @param minCapacity the desired minimum capacity*/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 codeif (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);queue = Arrays.copyOf(queue, newCapacity);}private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}

3. 比较机制

offer(E e): boolean 向堆中添加元素,如果堆中没有元素,直接添加到 0 下标,如果堆中有元素,调用 siftUp() 方法;

siftUp(int k, E x): void 向上调整,如果传了比较器,调用 siftUpUsingComparator() 方法,否则调用 siftUpComparable() 方法;

siftUpUsingComparator(int k, E x): void 使用比较器,向上调整;

计算根节点下标,将根节点下标的元素保存在变量 e 里面;

假设比较器是 a.compareTo(b):

使用比较器(调用比较器的 compare() 方法)比较 x 和 e,如果大于等于 0(x 大于等于根节点的值),跳出循环,把 x 放到堆的下一个位置,此时建立的是一个小根堆;

如果小于 0(x 的值比根节点小),把 e 放到下一个位置,向上调整,直到 x 比某一个根节点大或者已经到 0 下标的位置,把 x 放到这个位置,此时建立的是一个小根堆;

因此比较器传 a.compareTo(b),建立的就是小根堆;

反之,比较器传 b.compareTo(b),建立的就是大根堆;

    public boolean offer(E e) {if (e == null)throw new NullPointerException();modCount++;int i = size;if (i >= queue.length)grow(i + 1);size = i + 1;if (i == 0)queue[0] = e;elsesiftUp(i, e);return true;}private void siftUp(int k, E x) {if (comparator != null)siftUpUsingComparator(k, x);elsesiftUpComparable(k, x);}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;}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;}

四、topK问题

以前 k 个最小的元素为例:

使用优先级队列进行排序:

    public int[] smallestK(int[] arr, int k) {PriorityQueue<Integer> heap = new PriorityQueue<>();for(int x : arr) heap.offer(x);int[] ret = new int[k];for(int i = 0; i < k; i++) {ret[i] = heap.poll();}return ret;}

如果数组中元素非常多,优先级队列可能存不下,并且这样做是效率不高的;

推荐的做法:

使用数组中前 k 个数据建立一个容量为 k 的大根堆(堆顶元素是 k 个元素中最大的);

遍历后续的元素,如果元素小于堆顶元素,那么堆顶元素一定不是前 k 个最小的元素之一,堆顶元素出堆,新元素入堆;

    public int[] smallestK(int[] arr, int k) {int[] ret = new int[k];if(k == 0) return ret;PriorityQueue<Integer> heap = new PriorityQueue<>((a, b) -> b - a);for(int i = 0; i < k; i++) heap.offer(arr[i]);int n = arr.length;for(int i = k; i < n; i++){if(arr[i] < heap.peek()){heap.poll();heap.offer(arr[i]);}}for(int i = 0; i < k; i++){ret[i] = heap.poll();}return ret;}

找前 k 小的元素,建大根堆;找前 k 大的元素,建小根堆;

堆中元素就是前 k 小或者大的元素,如果是找第 k 小或者第 k 大元素,直接返回堆顶元素即可; 

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

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

相关文章

【版本控制教程】如何使用Unreal Engine 5 + UE源代码控制(Perforce P4)

本文来源perforce.com&#xff0c;由Perforce中国授权合作伙伴——龙智翻译整理&#xff0c;旨在为国内用户提供一份实用、易懂的Unreal Engine 5Perforce P4的中文使用指南。希望能为UE开发者、设计师和美术小伙伴们的版本控制实践提供有力支持~ Unreal Engine 5 是一款尖端的…

opensingleComDialog方法解析优化

下面是对 opensingleComDialog 方法的详细解析&#xff0c;并给出优化建议和优化后的代码。 方法解析 作用 opensingleComDialog(index) 方法用于在输入框失去焦点时&#xff08;blur 事件&#xff09;自动根据输入内容进行唯一性查询&#xff0c;如果查到唯一结果则自动填充…

css 实现1个像素在不同分辨率屏幕上画网格线

实现网格线绘制&#xff0c;要考虑画布style尺寸和画布像素大小的缩放关系 单像素绘制主要出现的问题是会模糊&#xff0c;从像素角度看就是出现绘制两个像素&#xff0c;实际就是要做偏移 核心就是&#xff1a;按物理像素绘制&#xff0c;首先要对齐物理像素&#xff0c;计算…

深度图聚类DGC—Paper Notes

目录 Unsupervised Deep Embedding for Clustering Analysis (DEC 2016)Attributed Graph Clustering: A Deep Attentional Embedding Approach (DAEGC 2019)Structural Deep Clustering Network (SDCN 2020)Contrastive Multi-View Representation Learning on Graphs (MVG…

获取YARN application 应用列表的几种方法

目录 1. 使用YARN命令行工具 2. 通过REST API获取 YARN 提供了获取YARN集群上运行的应用列表,以下是几种常见方法: 1. 使用YARN命令行工具 最直接的方式是使用YARN提供的命令行工具: yarn application -list 上述命令会显示所有正在运行的应用。 如果要查看所有应用(…

前端如何下载 ‘Content-Type‘: ‘application/octet-stream‘ 的文件

前言 在前端开发中&#xff0c;经常会遇到需要从后端接口下载文件的需求。当后端返回的响应头中 Content-Type 为 application/octet-stream 时&#xff0c;表示这是一个二进制流文件&#xff0c;浏览器无法直接展示&#xff0c;需要前端处理后下载到本地。本文将详细介绍前端…

咨询顾问进阶——顾问公司战略咨询分析模板【附全文阅读】

该战略咨询分析模板围绕企业战略分析展开&#xff0c;先从总体思考战略分析的目的与方法&#xff0c;接着探讨企业及战略定义、战略地位等。外部环境分析通过 PEST、五种竞争力等模型&#xff0c;分析环境、行业、市场等情况以发现机会与威胁&#xff1b;内部环境分析从资源、核…

宝塔服务器调优工具 1.1(Opcache优化)

第一步&#xff1a;宝塔服务器调优工具 1.1&#xff08;按照下面的参数填写&#xff09; 第二步&#xff1a;路径/www/server/php/80/etc/php.ini 搜索jit jit1235 其中1235根据服务器情况修改 第三步&#xff1a;路径/www/server/php/80/etc/php-cli.ini 搜索 jit1235 其中…

React Native【详解】动画

基础动画的实现流程 使用支持动画的组件 <Animated.Viewstyle{[{opacity: fadeAnim, // 绑定透明度动画值},]}><Text>动画元素</Text></Animated.View>Animated.View&#xff1a;用于创建动画容器&#xff0c;支持所有 View 的属性。Animated.Te…

如何轻松地将照片从 iPhone 传输到计算机

如果您的照片占据了 iPhone 上最多的存储空间&#xff0c;为什么不将照片从 iPhone 传输到电脑呢&#xff1f;您可能想要这样做&#xff0c;但不知道如何开始&#xff1f;如果是这样&#xff0c;那么本指南就是您所需要的。我们分享了 6 种方法以及步骤详细信息。您可以按照一种…

操作系统之内存管理(王道)

本篇博客依据王道、与我的笔记而写&#xff0c;讲解了内存的基础知识、内存管理的概念、进程的映像、连续分配管理方式、动态分区分配算法、基本分页存储管理、基本地址变换机构、TLB快表、两级页表、基本分段存储管理方式、段页式存储管理方式、虚拟内存、请求分页管理方式、页…

C++11 std::thread 多线程编程详解

C++11 标准首次将多线程支持引入语言标准库,其中最核心的部分就是 <thread> 头文件中的 std::thread 类。 🧱 一、基本概念 什么是线程? 线程是操作系统调度 CPU 时间的基本单位。一个进程中可以有多个线程,它们共享进程的资源(如内存、堆栈),但拥有各自独立的…

设置vscode使用eslint

在 Visual Studio Code (VSCode) 中设置 ESLint 是一个很好的方式来确保代码质量和一致性。以下是详细的步骤&#xff1a; 1. 安装 ESLint 扩展 打开 VSCode。点击左侧的扩展图标&#xff08;四边形图标&#xff09;。在搜索框中输入 ESLint。找到由 dbaeumer 提供的 ESLint …

.NET 生态中主流的前后端生产级框架

文章目录 **1. 后端框架&#xff08;Backend Frameworks&#xff09;****(1) ASP.NET Core**&#xff08;微软官方&#xff0c;主流选择&#xff09;**(2) ABP Framework**&#xff08;企业级应用开发框架&#xff09; **2. 前端框架&#xff08;Frontend Frameworks&#xff0…

Spring Cloud Alibaba整合Sentinel指南

目录 一、Sentinel核心功能概述 1. 控制台安装 2. 项目依赖配置 三、详细整合步骤 1. 基础配置 2. 资源定义与保护 3. 与OpenFeign整合 四、常见问题解决方案 五、最佳实践案例 1. 流量控制场景 2. 熔断降级场景 3. 热点参数限流 六、高级功能 Spring Cloud Aliba…

Win10+PHPStudy 8.1完美运行CRMEB开源商城(附性能优化配置)

环境配置 下载phpstudy https://www.xp.cn/ 安装完成之后打开&#xff0c;在软件管理中安装 nginx mysql 5.7 php 7.4 创建站点 填写域名&#xff0c;根目录选择到public文件夹下 创建完成之后&#xff0c;点击右侧管理&#xff0c;选择伪静态 location / { if (!-e $request…

康谋方案 | ARXML 规则下 ECU 总线通讯与 ADTF 测试方案

目录 一、引言 二、汽车电子控制系统 三、ECU开发流程中总线通讯&#xff1a;ARXML 规则下的标准化协作 四、ADTF&#xff1a;汽车数据与时间触发框架&#xff08;Automotive Data and Time-Triggered Framework&#xff09; 五、应用案例 六、结语 一、引言 随着汽车新…

常见JavaScript 代理模式应用场景解析

常见JavaScript 代理模式应用场景解析 在 JavaScript 开发中&#xff0c;代理模式&#xff08;Proxy Pattern&#xff09; 是一种强大的设计模式&#xff0c;它允许我们通过创建一个“代理”来控制对目标对象的访问。通过代理&#xff0c;我们可以拦截并增强对象的行为&#x…

暴雨信创电脑代理商成功中标长沙市中医康复医院

6月25日&#xff0c;国内科技产业领军企业暴雨信息传来喜讯&#xff0c;其信创电脑成功中标长沙市中医康复医院信息化设备采购项目。此次中标&#xff0c;不仅彰显了暴雨信息在信创领域的技术实力和产品优势&#xff0c;也为长沙市中医康复医院的信息化建设注入了新的活力。 长…

ZYNQ PL高速采集AD7606数据与QT动态显示全解析

从硬件设计到软件优化,打造工业级数据采集系统 在工业自动化、医疗仪器等领域,高速多通道数据采集系统至关重要。本文手把手教你基于Xilinx ZYNQ平台,实现8通道200kSPS高速采集**,并通过QT实现60fps动态波形显示。突破性采用五级流水采集架构和GPU加速渲染,解决传统方案的…