【数据结构篇】堆

文章目录

    • 前言
    • 基本介绍
      • 认识堆
      • 堆的特点
      • 堆的分类
      • 堆的操作
      • 堆的常见应用
    • 堆的实现
      • JDK 自带的堆
      • 手动实现堆

前言

本文主要是对堆的一个简单介绍,如果你是刚学数据结构的话,十分推荐看这篇文章,通过本文你将对堆这个数据结构有一个大致的了解,同时学习JDK自带的堆实现类PriorityQueue类,如何基于数组手写一个堆。

基本介绍

认识堆

  • 什么是堆

    堆(Heap)是一种常见的数据结构,堆可以基于数组实现,也可以基于链表实现。堆的定义如下:n个元素的序列 { k 1 , k 2 , k i , … , k n } {\{k1,k2,ki,…,kn\}} {k1,k2,ki,,kn} 当且仅当满足下关系 ( k i < = k 2 i , k i < = k 2 i + 1 ) (ki <= k2i,ki <= k2i+1) (ki<=k2i,ki<=k2i+1)或者 ( k i > = k 2 i , k i > = k 2 i + 1 ) , ( i = 1 , 2 , 3 , 4... n / 2 ) (ki >= k2i,ki >= k2i+1), (i = 1,2,3,4...n/2) (ki>=k2i,ki>=k2i+1),(i=1,2,3,4...n/2)时,称之为堆。堆是具备完全二叉树的特点的,二叉堆就是一种特别的完全二叉树,多叉堆也具有完全二叉树的特点。

    备注:完全二叉树的特点是,除了最后一层,其他层的叶子节点都是满的,也就是非最后一层节点的要么是2要么是0,这个度是就是子节点的个数,堆也是具有这个特点的

以下是一些常见的堆:

  • 最大堆:父节点永远是最大的,顶点为堆中最大元素

          9/ \7   8/ \ / \6  5 2  3/ \
    1   4
    
  • 最小堆:父节点永远是最小的,顶点为堆中最小元素

    		  0/   \1     2/  \   /  \6   5  7   3/ \ 9   8/
    4
    
  • 多叉堆:

              0/ / \ \/  /   \  \2   3    4  5/ | \ 6  7  8

堆的特点

  • 堆的特点
    • 类完全二叉树:堆具有完全二叉树的特点,除了最后一层外,其他层的节点都是满的,并且最后一层的节点从左到右连续排列。
    • 堆序性质:堆中的每个节点都满足堆序性质。对于最小堆来说,任意节点的值都小于等于其子节点的值;而对于最大堆来说,任意节点的值都大于等于其子节点的值。
    • 根节点存储极值:在最小堆中,根节点存储着堆中的最小值;而在最大堆中,根节点存储着堆中的最大值。因此,可以通过堆的根节点快速获取极值。
    • 快速插入和删除:堆支持高效的插入和删除操作。在最小堆中,插入新元素和删除最小元素的时间复杂度均为 O(log n),其中 n 是堆中元素的数量。最大堆的插入和删除最大元素操作也具有相同的时间复杂度。

堆的分类

  • 堆的分类
    • 按照结构可以分为
      • 二叉堆:每个节点最多有两个子节点的堆。二叉堆分为最大堆和最小堆。
      • 多叉堆:每个节点可以拥有多个子节点的堆,其中特别常见的是二叉堆的扩展,也就是三叉堆、四叉堆等。
    • 按照顺序可以分为
      • 最大堆:在最大堆中,父节点的值大于或等于其子节点的值。堆顶元素是最大值。
      • 最小堆:在最小堆中,父节点的值小于或等于其子节点的值。堆顶元素是最小值。

所以也可以组合命名,比如:多叉最小堆、多叉最大堆、二叉最小堆、二叉最大堆,其中最为常见的就是二叉最大堆(一般直接简称最大堆)和二叉最小堆(一般直接简称最小堆),而本文中的堆实现就是主要讲解 最大堆 和 最小堆

堆的操作

  1. 插入元素:将一个新元素插入到堆中。插入操作通常是在堆的末尾添加新元素,然后通过上浮操作(Heapify Up)或下浮操作(Heapify Down)调整堆的结构,以满足堆的性质。
  2. 删除元素:从堆中删除指定元素。通常情况下,需要删除的元素是位于堆的根节点。删除操作会将根节点与最后一个叶子节点交换,然后通过下沉操作(Heapify Down)调整堆的结构,以满足堆的性质。
  3. 获取极值:获取堆中的最大值或最小值(取决于是最大堆还是最小堆)。在最大堆中,最大值存储在根节点;在最小堆中,最小值存储在根节点。获取极值操作可以在 O(1) 的时间复杂度内完成。
  4. 堆化:通过上浮操作或下沉操作,调整堆的结构,使之满足堆的性质。上浮操作用于维护插入元素后的堆性质,而下沉操作用于维护删除元素后的堆性质。
  5. 构建堆:将一个无序数组转换为堆的过程。构建堆的常见方法是从数组最后一个非叶子节点开始,依次进行下沉操作,以保证每个节点都满足堆的性质。
  • 堆的上浮和下浮操作

    • 上浮(Heapify Up):也称为向上调整或上升,是指在插入元素到堆时将其移动到正确的位置以满足堆的性质。对于最大堆来说,上浮操作是将新插入的元素不断与其父节点进行比较和交换,直到满足堆的性质。具体步骤如下:

      • Step1:将新插入的元素放在堆数组的末尾。
      • Step2:将该元素与父节点进行比较,如果它比父节点大(对于最大堆),则交换两者位置。
      • Step3:重复上述比较和交换的过程,直到新插入的元素达到合适的位置或成为根节点。

      上浮操作保证了插入后的堆仍然保持堆的性质,即对于最大堆,父节点的值大于等于子节点的值。

      image-20230930170714265

    • 下沉(Heapify Down):也称为向下调整或下降,是指在删除堆顶元素后,将最后一个元素移到堆顶并将其下沉到正确的位置以满足堆的性质。对于最大堆来说,下沉操作是将堆顶元素不断与其子节点进行比较和交换,直到满足堆的性质。具体步骤如下:

      • Step1:将堆数组的最后一个元素移到堆顶位置。
      • Step2:将堆顶元素与它的子节点进行比较,选择较大的子节点。
      • Step3:如果堆顶元素小于较大的子节点(对于最大堆),则交换两者位置。
      • Step4:重复上述比较和交换的过程,直到堆顶元素达到合适的位置或成为叶子节点。

      下沉操作保证了删除堆顶元素后的堆仍然保持堆的性质,即对于最大堆,父节点的值大于等于子节点的值。

      image-20230930194114849

堆的常见应用

基于堆的特点,堆的常见应用有:

  1. 优先队列(Priority Queue):堆常被用于实现优先队列,其中元素按照优先级进行排序。通过堆的性质,可以快速插入新元素和获取当前最高优先级的元素。
  2. 堆排序(Heap Sort):堆排序是一种基于堆的排序算法,它利用堆的属性进行排序。堆排序是一种原地、稳定的排序算法,具有较好的平均和最坏时间复杂度,适用于大规模数据集的排序。
  3. 图算法中的最短路径和最小生成树:堆被广泛用于图算法中的最短路径和最小生成树问题。例如,Dijkstra算法使用最小堆来选择下一个距离顶点最短的节点,Prim算法使用最小堆来选择下一个距离树最近的边。
  4. 模拟系统中的事件调度:在模拟系统中,堆可用于事件驱动的调度和处理。每个事件都具有优先级,堆的结构使得可以快速找到下一个最高优先级的事件进行处理。
  5. 中位数的查找:通过使用两个堆,分别维护数据流的较小部分和较大部分,可以高效地查找中位数。
  6. 数据压缩和哈夫曼编码:堆可用于构建哈夫曼树,用于数据压缩和编码。根据字符频率构建最小堆,然后按照频率合并节点,生成哈夫曼树,并将字符编码为可变长度的前缀码。

堆的实现

JDK 自带的堆

在Java中,有一个名为PriorityQueue的类实现了堆(Heap)的功能。PriorityQueue是一个优先队列,基于堆的数据结构实现。

使用PriorityQueue可以方便地操作堆的插入、删除和获取极值等操作。它根据元素的优先级进行排序,并保证每次操作都能够高效地获取最高优先级的元素。

以下是一些PriorityQueue常用方法的示例:

  • add(E e)offer(E e):网堆中添加元素。将元素插入优先队列。
  • remove()poll():删除堆的根节点。删除并返回队列中的最高优先级元素。
  • peek():获取极值。返回队列中的最高优先级元素,但不删除。
  • size():获取堆中元素的数量。返回队列中的元素个数。

至于堆化和构建堆的操作 JDK 底层自动实现了,我们只管调用无需关心实现,这就是 Java 的好处吧

备注PriorityQueue默认是最小堆(小顶堆),即优先级较低的元素具有更高的优先级。如果需要使用最大堆(大顶堆),可以通过提供自定义的Comparator来实现。

示例

示例一:最小堆(默认)

堆顶元素永远是最小的

import java.util.PriorityQueue;public class HeapExample {public static void main(String[] args) {PriorityQueue<Integer> minHeap = new PriorityQueue<>();minHeap.add(5);minHeap.add(2);minHeap.add(8);System.out.println(minHeap.peek()); // 输出: 2System.out.println(minHeap.poll()); // 输出: 2System.out.println(minHeap.peek()); // 输出: 5System.out.println(minHeap.size()); // 输出: 2}
}

示例二:最大堆

堆顶元素永远是最大的

import java.util.Comparator;
import java.util.PriorityQueue;public class MaxHeapExample {public static void main(String[] args) {PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());maxHeap.add(5);maxHeap.add(2);maxHeap.add(8);System.out.println(maxHeap.peek()); // 输出: 8System.out.println(maxHeap.poll()); // 输出: 8System.out.println(maxHeap.peek()); // 输出: 5System.out.println(maxHeap.size()); // 输出: 2}
}

手动实现堆

注意:以下实现都是基于数组实现的,基于链表实现的我就没有写了,一般而言堆都是基于数组实现的,因为数组相对链表,内存是连续的,不需要存引用,相较而言,查询性能更好,内存占用也会相对少很多

手写堆提供的API

  • Heap():创建堆的无参构造方法,默认容量为10

  • Heap(int capacity):创建堆的有参构造方法,指定堆的初始容量

  • add(int val):往堆中添加一个元素,超过容量会自动进行扩容,扩容为原来容量的两倍

  • remove():移除并返回堆顶的元素(也就是极大值)

  • remove(int val):移除指定元素并返回,如果不存在抛异常

  • printHeap():打印堆中的元素,相当于是层序遍历二叉树

  • isEmpty():判断堆是否为空

/*** @author ghp* @title* @description*/
public class Heap {/*** 堆的默认容量*/private static final int DEFAULT_CAPACITY = 10;/*** 堆数组,用于存储堆中的元素*/private int[] heapArray;/*** 堆的容量*/private int capacity;/*** 堆中元素的数量*/private int size;public Heap() {this.capacity = DEFAULT_CAPACITY;this.heapArray = new int[capacity];this.size = 0;}public Heap(int capacity) {this.capacity = capacity;this.heapArray = new int[capacity];this.size = 0;}/*** 遍历打印堆中所有的元素*/public void printHeap() {for (int i = 0; i < size; i++) {System.out.print(heapArray[i] + " ");}System.out.println();}/*** 判断堆是否为空** @return*/public boolean isEmpty() {return size == 0;}/*** 往队中添加一个元素** @param value*/public void add(int value) {if (size == capacity) {// 扩容为当前容量的两倍resize(capacity * 2);}heapArray[size] = value;heapifyUp(size++);}/*** 移除堆顶元素** @return*/public int remove() {if (isEmpty()) {throw new IllegalStateException("Heap is empty");}int max = heapArray[0];heapArray[0] = heapArray[--size];heapifyDown(0);return max;}/*** 移除堆中指定元素** @param value*/public void remove(int value) {int index = findIndex(value);if (index == -1) {throw new IllegalArgumentException("Element does not exist in the heap");}// 使用最后一个节点覆盖要删除的元素,这样才能确保节点都能进行一个更新heapArray[index] = heapArray[--size];heapifyDown(index);}/*** 寻找到指定元素的索引** @param value* @return*/private int findIndex(int value) {for (int i = 0; i < size; i++) {if (heapArray[i] == value) {return i;}}return -1;}/*** 上浮(新增操作时节点上移)** @param index*/private void heapifyUp(int index) {// 定位父节点int parent = (index - 1) / 2;if (parent >= 0 && heapArray[parent] < heapArray[index]) {// 如果父节点比子节点小,则进行交换,然后递归上浮,确保父节点是永远大于子节点的swap(parent, index);heapifyUp(parent);}}/*** 下浮(删除操作时节点下移)** @param index*/private void heapifyDown(int index) {// 左节点的索引int left = 2 * index + 1;// 右节点的索引int right = 2 * index + 2;// 父节点索引int parent = index;// 判断左右节点是否大于父节点if (left < size && heapArray[left] > heapArray[parent]) {parent = left;}if (right < size && heapArray[right] > heapArray[parent]) {parent = right;}if (parent != index) {// 如果父节点发生了更新,则交换父节点和子节点的位置,同时递归下浮,确保父节点永远是最大的swap(parent, index);heapifyDown(parent);}}/*** 交换堆数组索引为 i 和 j 的两个元素** @param i* @param j*/private void swap(int i, int j) {int temp = heapArray[i];heapArray[i] = heapArray[j];heapArray[j] = temp;}/*** 扩容堆大小** @param newCapacity*/private void resize(int newCapacity) {if (newCapacity < DEFAULT_CAPACITY){newCapacity = DEFAULT_CAPACITY;}if (newCapacity < 0) {newCapacity = Integer.MAX_VALUE;}heapArray = Arrays.copyOf(heapArray, newCapacity);capacity = newCapacity;}
}

备注:如果想要使用最小堆,则只需要修改上浮和下浮的if判断条件即可

测试:

public class Test {public static void main(String[] args) {Heap heap = new Heap(0);heap.add(1);heap.add(2);heap.add(3);heap.add(4);heap.add(5);heap.printHeap();heap.remove(1);heap.printHeap();}
}

image-20230930194253344

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

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

相关文章

C++ 传值调用

向函数传递参数的传值调用方法&#xff0c;把参数的实际值复制给函数的形式参数。在这种情况下&#xff0c;修改函数内的形式参数不会影响实际参数。 默认情况下&#xff0c;C 使用传值调用方法来传递参数。一般来说&#xff0c;这意味着函数内的代码不会改变用于调用函数的实…

阿里云ACP知识点(三)

1、弹性伸缩不仅提供了在业务需求高峰或低谷时自动调节ECS实例数量的能力&#xff0c;而且提供了ECS实例上自动部署应用的能力。弹性伸缩的伸缩配置支持多种特性&#xff0c;例如______,帮助您高效、灵活地自定义ECS实例配置&#xff0c;满足业务需求。 标签、密钥对、 实例RAM…

大学各个专业介绍

计算机类 五米高考-计算机类 注&#xff1a;此处平均薪酬为毕业五年平均薪酬&#xff0c;薪酬数据仅供参考 来源&#xff1a; 掌上高考 电气类 五米高考-电气类 机械类 五米高考-机械类 电子信息类 五米高考-电子信息类 土木类 五米高考-土木类

【多媒体技术与实践】音频信息获取和处理——编程题汇总

1&#xff1a;音频信息数据量计算 已知采样频率&#xff08;单位KHz&#xff09;、量化位数、声道数及持续时间&#xff08;单位分钟&#xff09;&#xff0c;求未压缩时的数据量&#xff08;单位MB&#xff09;. 例如&#xff1a; 输入&#xff1a; 22.05 16 2 3 &#xff…

从零手搓一个【消息队列】实现数据的硬盘管理和内存管理(线程安全)

文章目录 一、硬盘管理1, 创建 DiskDataCenter 类2, init() 初始化3, 封装交换机4, 封装队列5, 关于绑定6, 关于消息 二、内存管理1, 数据结构的设计2, 创建 MemoryDataCenter 类3, 关于交换机4, 关于队列5, 关于绑定6, 关于消息7, 恢复数据 三、小结 创建 Spring Boot 项目, S…

26 docker前后端部署

[参考博客]((257条消息) DockerNginx部署前后端分离项目(SpringBootVue)的详细教程_在docker中安装nginx实现前后端分离_这里是杨杨吖的博客-CSDN博客) (DockerNginx部署前后端分离项目(SpringBootVue)) 安装docker # 1、yum 包更新到最新 yum update # 2、安装需要的软件包…

SEO的优化教程(百度SEO的介绍和优化)

百度SEO关键字介绍&#xff1a; 百度SEO关键字是指用户在搜索引擎上输入的词语&#xff0c;是搜索引擎了解网站内容和相关性的重要因素。百度SEO关键字可以分为短尾词、中尾词和长尾词&#xff0c;其中长尾词更具有针对性和精准性&#xff0c;更易于获得高质量的流量。蘑菇号-…

构建一个TypeScript环境的node项目

本文 我们用一种不太一样的方式来创建项目 这里 我们事先创建了一个文件夹作为项目目录 然后打开项目终端 输入 npm init然后 在新弹出的对话框中 大体就是 名字随便写一个 然后 后面的回车&#xff0c;到最后一个输入 yes 然后回车 这样 我们就有一个基础的 node项目结构了…

AGV小车、机械臂协同作业实战06-任务分配算法(图解蚁群算法)代码示例java

什么是蚁群算法&#xff1f; 蚁群系统(Ant System(AS)或Ant Colony System(ACS))是由意大利学者Dorigo、Maniezzo等人于20世纪90年代首先提出来的。他们在研究蚂蚁觅食的过程中&#xff0c;发现蚁群整体会体现一些智能的行为&#xff0c;例如蚁群可以在不同的环境下&#xff0c…

排序篇(四)----归并排序

排序篇(四)----归并排序 1.归并(递归) 基本思想&#xff1a; 归并排序&#xff08;MERGE-SORT&#xff09;是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide andConquer&#xff09;的一个非常典型的应用。将已有序的子序列合并&#xff0c;得到…

Hive SQL初级练习(30题)

前言 Hive 的重要性不必多说&#xff0c;离线批处理的王者&#xff0c;Hive 用来做数据分析&#xff0c;SQL 基础必须十分牢固。 环境准备 建表语句 这里建4张表&#xff0c;下面的练习题都用这些数据。 -- 创建学生表 create table if not exists student_info(stu_id st…

rabbimq之java.net.SocketException: Connection reset与MissedHeartbeatException分析

一、前言 在android前端中接入了rabbitmq消息队列来处理业务&#xff0c;在手机网络环境错综复杂&#xff0c;网络信号不稳定&#xff0c;可能导致mq的频繁断开与连接&#xff0c;在日志中&#xff0c;发现有很多这样的日志&#xff0c;java.net.SocketException: Connection …

yolov5分割+检测c++ qt 中部署,以opencv方式(详细代码(全)+复制可用)

1&#xff1a;版本说明&#xff1a; qt 5.12.10 opencv 4.5.3 &#xff08;yolov5模型部署要求opencv>4.5.0&#xff09; 2&#xff1a;检测的代码 yolo.h #pragma once #include<iostream> #include<cmath> #include<vector> #include <opencv2/…

【QandA C++】内存分段和内存分页等重点知识汇总

目录 内存分段 内存分页 内存分段 程序是由若干个逻辑分段组成的&#xff0c;如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的&#xff0c;所以就用分段的形式把这些段分离出来。 分段机制下&#xff0c;虚拟地址和物理地址是如何映射的&#xff1f; …

毅速课堂:3D打印随形水路在小零件注塑中优势明显

小零件注塑中的冷却不均匀问题常常导致烧焦现象的发生。这主要是因为传统机加工方法无法制造出足够细小的水路&#xff0c;以适应小零件的复杂形状。而3D打印技术的引入&#xff0c;尤其是随形水路的设计&#xff0c;为解决这一问题提供了新的解决方案。 3D打印随形水路技术的优…

TS编译选项——编译TS文件同时对JS文件进行编译

一、允许对JS文件进行编译 我们在默认情况下编译TS项目时是不能编译js文件的&#xff0c;如下图中的hello.js文件并未编译到dist目录下&#xff08;这里配置了编译文件放到dist目录下&#xff09; 如果我们想要实现编译TS文件同时对JS文件进行编译&#xff0c;就需要在tsconfi…

列出使用Typescript的一些优点?

使用Typescript有以下优点&#xff1a; 类型安全&#xff1a;Typescript是一种静态类型语言&#xff0c;它要求在编码阶段明确定义变量和函数的类型。这种类型安全可以减少在运行时出现错误的可能性&#xff0c;并提高代码的可读性和可维护性。代码可读性和可维护性&#xff1…

使用U3D、pico开发VR(二)——添加手柄摇杆控制移动

一、将unity 与visual studio 相关联 1.Edit->Preference->External tool 选择相应的版本 二、手柄遥控人物转向和人物移动 1.添加Locomotion System组件 选择XR Origin&#xff1b; 2.添加Continuous Move Provider&#xff08;Action-based&#xff09;组件 1>…

Android - kts文件配置应用签名

升级最新的AndroidStudio后&#xff0c;gradle配置文件从Groovy 迁移到 KTS&#xff0c;这里把自己配置应用签名遇到的问题及注意事项分享下。 Google官方说明地址将 build 配置从 Groovy 迁移到 KTS 配置后的代码如下&#xff1a; signingConfigs {create("keyStore&q…

PHP 反序列化漏洞:手写序列化文本

文章目录 参考环境序列化文本Scalar Type整数浮点数布尔值字符串 Compound Type数组数据结构序列化文本 对象数据结构序列化文本 Special TypeNULL数据结构序列化文本 手写序列化文本过程中的注意事项个数描述须于现实相符序列化文本前缀的大小写变化符号公共属性 参考 项目描…