经典数据结构——堆的实现

一、完全二叉树

堆是一种完全二叉树,什么是完全二叉树?

简单的说,一棵满二叉树表示的是所有节点全部饱和,最后一层全部占满:

而完全二叉树指的是满二叉树的最后一层,所有叶子节点都从左往顺序排满:

完全二叉树的特点非常简单,除了最后一层,其他各层节点都是满的,而最后一层,所有节点从左往右依次排满。它并不关心节点元素的大小,只与这一结构特点有关。

二、堆结构

前面说到,堆是一种特殊的完全二叉树,除了符合完全二叉树的结构特点,它还有另一个特性,由这个特性,我们又可以将堆分为——大根堆、小根堆

大根堆:每个节点比它的子节都要大。

小根堆:和大根堆相反,每个节点比它的子节点都要小。

注意,堆只关心父节点与子节点之间的大小,要么父节点比子节点都大,视为大根堆;要么父节点比子节点都小,视为小根堆。只要保证这一点,再加上完全二叉树的特点,就是一个堆结构。至于全部节点是否呈现一种左大右小或左小右大的关系,堆并不关心。

如何实现一个堆?

堆并不是一个实际存在的物理结构,它需要通过一个一维数组来表示。实际上,数组表示了堆的一种对应关系。

有了这样一种对应关系,我们可以得到下面 3 个公式:

已知任意位置 index,求其父节点和子节点位置:

父节点:int fatherIdx = (index - 1) / 2; // 注意, (0 - 1) / 2 = 0,实际上double->int 是向0取整,或绝对值向下取整。

左子节点:int leftIdx = index * 2 + 1;

右子节点:int rightIdx = index * 2 + 2;

例如,4 位置的父节点是 1 位置,(4 - 1) / 2 = 1 ,向下取整

有了位置关系,剩下的工作就是实现大根堆或小根堆,一般情况,大根堆可以快速返回整个堆中的最大值,比较常用,以下就以大根堆为例。

接下来,我们通过代码来详细解析如何实现一个堆结构。

三、堆结构的实现

以数组为基础,构建一个大根堆的对应关系。这个堆结构需要实现以下几个公共方法:

  1. push 入堆
  2. pop 出堆
  3. isEmpty 是否为空
  4. isFull 是否已满

实现堆结构的过程要始终紧扣两个特点:

  1. 完全二叉树的特点
  2. 大根堆的特点

只有这两点满足,它才是一个正确的堆。

当push一个元素的时候,由于实现结构是数组,因此始终是追加到数组的末尾,但我们可以通过特定的方法来实现“堆结构”的调整。

想象这个元素被追加到了堆结构最后一层的末尾,首先完全二叉树的条件就满足了。

但是大根堆的特点呢?这时就需要从末尾开始,向上与父节点比大小,大的站在父节点的位置,然后重复这个过程,直到这个元素不再比父节点大,或已经站在了 0 的位置。

    public void push(int value) {if (isFull())throw new RuntimeException("heap is full");heap[heapSize] = value;heapInsert(heap, heapSize++);}private void heapInsert(int[] arr, int index) {// 当前位置元素比父节点大,交换位置,重置当前位置// 循环条件有两点作用:1、当前节点>父节点(明显)// 2、由于 (0-1)/2=0,如果index已经是0位置,出现相等的情况,跳出循环(隐藏)while (arr[index] > arr[(index - 1) / 2]) {SortUtil.swap(arr, index, (index - 1) / 2);index = (index - 1) / 2;}}

而如果 pop 一个元素,稍微复杂一些,首先,我们需要记录下 0 位置上的元素,然后用堆的最后一个元素补位,heapSize 缩减 1 位,这几步操作是为了保证取出元素后依然是一个完全二叉树。

然后我们需要从 0 位置上(已经替换为最后一个位置上的数)与子节点比较,找到最大的子节点,然后与其交换(下沉),循环直到下沉到“堆的最后一层”或子节点都比自己小(终止条件)

    public int pop() {int max = heap[0];// 1、要拿掉根节点,因为要保证是一个完全二叉树,所以第一步,我们取最后一个元素补位SortUtil.swap(heap, 0, heapSize - 1);// 2、heapSize缩减一位heapSize--;// 2、再执行heapify下沉操作,因为补位的元素可能不满足大根的特点,所以要向下比较heapify(heap, 0, heapSize);return max;}private void heapify(int[] arr, int i, int heapSize) {int left = 2 * i + 1;// 若左孩子没有越界,证明存在下一级,有可能需要下沉while (left < heapSize) {// 选出子节点中最大的那个位置int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;largest = arr[largest] > arr[i] ? largest : i;if (largest == i)break;else {// 执行交换SortUtil.swap(arr, largest, i);i = largest;left = 2 * i + 1;}}}

以下是完整代码:

/*** 大根堆** @data 2021/5/15 16:46*/
public class Code2_MaxHeap {/*** 堆容器*/private int[] heap;/*** 元素限制*/private final int limit;/*** 堆大小*/private int heapSize;public Code2_MaxHeap(int limit) {this.heap = new int[limit];this.limit = limit;this.heapSize = 0;}public void push(int value) {if (isFull())throw new RuntimeException("heap is full");heap[heapSize] = value;heapInsert(heap, heapSize++);}/*** 1、因为是数组表示的堆结构,每次插入都是在末尾,因此,index每次都是堆的最后一个值* 2、利用堆结构的特点,可以快速求出当前位置的父节点在数组中的下标,即(index - 1)/2* 3、比较当前位置与父节点 大小,如果比父大,交换,然后当前位置变为父节点位置* 4、重复 2、3,继续向上比较,要么直到没有父节点,那么while的条件会在两数相等时退出,要么不比父节点大,也停止向上比较。*/private void heapInsert(int[] arr, int index) {// 当前位置元素比父节点大,交换位置,重置当前位置while (arr[index] > arr[(index - 1) / 2]) {SortUtil.swap(arr, index, (index - 1) / 2);index = (index - 1) / 2;}}public int pop() {int max = heap[0];// 1、要拿掉根节点,因为要保证是一个完全二叉树,所以第一步,我们取最后一个元素补位SortUtil.swap(heap, 0, heapSize - 1);// 2、heapSize缩减一位heapSize--;// 2、再执行heapify下沉操作,因为补位的元素可能不满足大根的特点,所以要向下比较heapify(heap, 0, heapSize);return max;}private void heapify(int[] arr, int i, int heapSize) {int left = 2 * i + 1;// 若左孩子没有越界,证明存在下一级,有可能需要下沉while (left < heapSize) {// 选出子节点中最大的那个位置int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;largest = arr[largest] > arr[i] ? largest : i;if (largest == i)break;else {// 执行交换SortUtil.swap(arr, largest, i);i = largest;left = 2 * i + 1;}}}public boolean isEmpty() {return heapSize == 0;}public boolean isFull() {return heapSize == heap.length;}}

测试代码:

    public static void main(String[] args) {int value = 1000;int limit = 100;int testTimes = 1000000;for (int i = 0; i < testTimes; i++) {int curLimit = (int) (Math.random() * limit) + 1;Code2_MaxHeap my = new Code2_MaxHeap(curLimit);RightMaxHeap test = new RightMaxHeap(curLimit);int curOpTimes = (int) (Math.random() * limit);for (int j = 0; j < curOpTimes; j++) {if (my.isEmpty() != test.isEmpty()) {System.out.println("Oops!");}if (my.isFull() != test.isFull()) {System.out.println("Oops!");}if (my.isEmpty()) {int curValue = (int) (Math.random() * value);my.push(curValue);test.push(curValue);} else if (my.isFull()) {if (my.pop() != test.pop()) {System.out.println("Oops!");}} else {if (Math.random() < 0.5) {int curValue = (int) (Math.random() * value);my.push(curValue);test.push(curValue);} else {if (my.pop() != test.pop()) {System.out.println("Oops!");}}}}}System.out.println("finish!");}/*** 遍历数组选出一个最大值 pop* 用于验证与自定义MaxHeap.pop的值是相等的。*/
class RightMaxHeap {private int[] arr;private final int limit;private int size;public RightMaxHeap(int limit) {arr = new int[limit];this.limit = limit;size = 0;}public boolean isEmpty() {return size == 0;}public boolean isFull() {return size == limit;}public void push(int value) {if (size == limit) {throw new RuntimeException("heap is full");}arr[size++] = value;}public int pop() {int maxIndex = 0;for (int i = 1; i < size; i++) {if (arr[i] > arr[maxIndex]) {maxIndex = i;}}int ans = arr[maxIndex];arr[maxIndex] = arr[--size];return ans;}
}

如果一个堆的某个位置的数变了,还不知道变大还是变小,如何重新调整堆结构? 这个位置 i 顺序调用一下 heapinsert 和 heapify 整个堆就会自动整理好。

四、优先级队列与堆

java.util.PriorityQueue<T>  是一个优先级队列数据结构,它的底层实现就是用堆来完成的。另外,它是允许添加重复元素的,这与 TreeMap 不允许添加重复元素有区别。

这种结构可以立刻返回最大值或最小值,指定一个比较器(参考《比较器的使用》),符合“正减升,反减降”的口诀。如果是按升序设定比较器,那么 peek 或 poll 方法就会返回最小值。反之就是最大值。

    public static void main(String[] args) {// 小根堆PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o1 - o2);heap.add(5);heap.add(5);heap.add(5);heap.add(3);//  5 , 3System.out.println(heap.peek());heap.add(7);heap.add(0);heap.add(7);heap.add(0);heap.add(7);heap.add(0);System.out.println(heap.peek());while (!heap.isEmpty()) {System.out.println(heap.poll());}}

五、堆的时间复杂度

堆结构的时间复杂度主要是看 heapInsert 和 heapify 两个方法。

它们的操作始终与树的高度紧密相关,每次只有一个节点调换,整体是复杂度都是 O(logN)。

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

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

相关文章

排序算法 —— 堆排序

引言 此文基于《经典数据结构——堆的实现》中堆结构&#xff0c;实现一个以堆处理排序的算法。 一、算法思想 基于堆结构的堆排序的算法思想非常简单&#xff0c;循环获取大根堆中的最大值&#xff08;0位置的根节点&#xff09;放到堆的末尾&#xff0c;直到将堆拿空。 由…

经典数据结构——前缀树

引言 前缀树——trie /ˈtraɪ//树&#xff0c;也叫作“单词查找树”、“字典树”。 它属于多叉树结构&#xff0c;典型应用场景是统计、保存大量的字符串&#xff0c;经常被搜索引擎系统用于文本词频统计。它的优点是利用字符串的公共前缀来减少查找时间&#xff0c;最大限度…

排序算法 —— 计数排序

引言 计数排序是桶排序思想的一种具体实现&#xff0c;针对一些具有特殊限制的样本数据&#xff0c;如公司员工年龄&#xff0c;那么样本数据本身就一定在0~200之间&#xff0c;针对这样的数据&#xff0c;使用从0到200 的桶数组&#xff0c;桶的位置已经是有序的&#xff0c;…

Java多线程 —— 线程状态迁移

引言 线程状态迁移&#xff0c;又常被称作线程的生命周期&#xff0c;指的是线程从创建到终结需要经历哪些状态&#xff0c;什么情况下会出现哪些状态。 线程的状态直接关系着并发编程的各种问题&#xff0c;本文就线程的状态迁移做一初步探讨&#xff0c;并总结在何种情况下…

Java中的Unsafe

Java和C语言的一个重要区别就是Java中我们无法直接操作一块内存区域&#xff0c;不能像C中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C手动管理内存的能力。 Unsafe类&#xff0c;全限定名是sun.misc.Unsafe&#xff0c;从名字中我们可以看出来这个类对…

arm中断保护和恢复_浅谈ARM处理器的七种异常处理

昨天的文章&#xff0c;我们谈了ARM处理器的七种运行模式&#xff0c;分别是&#xff1a;用户模式User(usr)&#xff0c;系统模式System(sys)&#xff0c;快速中断模式(fiq)&#xff0c;管理模式Supervisor(svc)&#xff0c;外部中断模式(irq)&#xff0c;数据访问中止模式Abor…

Queue —— JUC 的豪华队列组件

目录引言一、Queue 的继承关系1.1 Queue 定义基础操作1.2 AbstractQueue 为子类减负1.3 BlockingQueue 阻塞式Queue1.4 Deque 两头进出二、Queue 的重要实现三、BlockingQueue 的实现原理四、Queue 在生产者消费者模式中的应用五、Queue 在线程池中的应用六、ConcurrentLinkedQ…

daad转换器实验数据_箔芯片电阻在高温应用A/D转换器中的应用

工业/应用领域高温&#xff1a;地震数据采集系统、石油勘探监测、高精度检测仪产品采用&#xff1a;V5X5 Bulk Metal (R) Foil芯片电阻案例介绍TX424是一个完整的4通道24位模数转换器&#xff0c;采用40脚封装。该设计采用最先进设计方案&#xff0c;两个双通道24位调节器和一个…

excel分段排序_学会这个神操作,报表填报不再五花八门,效率远超Excel

在报表工作人员的的日常工作中&#xff0c;常常要面临统计混乱的终端用户输入的问题。由于无法准确限制用户的输入内容&#xff0c;所以在最终进行数据统计时&#xff0c;常常会出现数据不合法的情况。为此需要花费大量的人力和时间核对校验数据。举个简单的例子&#xff0c;某…

IDEA——必备插件指南

目录一、Free-Mybatis-Plugin二、Lombok三、jclasslib Bytecode Viewer一、Free-Mybatis-Plugin 二、Lombok 三、jclasslib Bytecode Viewer 学习 class 文件的必备插件。 使用简单&#xff0c;安装后可以在菜单 View 中看到 show bytecode with jclasslib&#xff1a; 效果…

jitter 如何优化网络_如何做好关键词优化网络?

越来越多的传统企业开始建立自己的网站&#xff0c;进而不断的推广自己的产品。为了能够让自己的企业网站出现在搜索引擎的首页&#xff0c;现在最常用的手段就是竞价排名和关键词优化网络。往往很多企业会选择关键词优化网络这种方式来推广自己的网站&#xff0c;对于新手seoe…

python学生名片系统_Python入门教程完整版400集(懂中文就能学会)快来带走

如何入门Python&#xff1f;权威Python大型400集视频&#xff0c;学了Python可以做什么&#xff1f;小编今天给大家分享一套高老师的python400集视频教程&#xff0c;里面包含入门进阶&#xff0c;源码&#xff0c;实战项目等等&#xff0c;&#xff0c;不管你是正在学习中&…

JVM——详解类加载过程

导航一、过程概述二、Loading2.1 类加载器2.2 双亲委派机制2.3 类在内存中的结构三、Linking四、Initializing一、过程概述 java 源文件编译后会生成一个 .class文件存储在硬盘上。 在程序运行时&#xff0c;会将用到的类文件加载到 JVM 内存中。从磁盘到内存的过程总共分为三…

下载 Java 学习的权威文档

JVMS 和 JLS 文档的下载 快速直达&#xff1a; https://docs.oracle.com/javase/8/ --> Java Language and Virtual Machine Specifications jvm specification 和 java language specification 是Java 学习的两个最权威的文档。如果你用的是 Java 8&#xff0c;就可以去下载…

iso图像测试卡_4700万像素 五轴防抖 徕卡正式发布SL2无反相机

出自蜂鸟网-器材频道&#xff0c;原文链接&#xff1a;https://m.fengniao.com/document/5358989.html徕卡于今日正式发布SL2相机&#xff0c;搭载4700万像素CMOS感光元件、通过感光元件移位实现光学图像稳定的五轴防抖技术、全新徕卡物距探测式自动对焦技术以及576万像素分辨率…

JVM——对象的创建与内存布局

导航一、对象的创建过程二、对象的内存布局2.1 内存布局2.2 计算对象的内存大小三、对象的定位3.1 句柄池3.2 直接指针四、对象的分配过程一、对象的创建过程 对象&#xff0c;又叫实例&#xff0c;是 OOP 的最常用角色。 如何创建一个对象&#xff1f;一般都是使用 new 关键…

JVM垃圾收集器——G1

导航引言一、G1 介绍1.1 适用场景1.2 设计初衷1.3 关注焦点1.4 工作模式1.5 堆的逻辑结构1.6 主要收集目标1.7 停顿预测模型1.8 拷贝和压缩1.9 与 CMS 和 Parallel 收集器的比较1.10 固定停顿目标二、堆的逻辑分区2.1 region2.2 CSet2.3 RSet2.4 Card Table三、G1 的工作原理3.…

的mvc_简述PHP网站开发的MVC模式

为了提高开发时候的代码重用和开发速度&#xff0c;php使用了mvc的模式&#xff0c;主要是对代码的功能进行了分类&#xff0c;M&#xff1a;model主要是对数据库进行操作&#xff0c;v&#xff1a;view主要是前端html文件操作&#xff0c;c&#xff1a;controller主要是编写基…

CAP 原则与 BASE 理论

导航引言一、CAP 原则1.1 Consistency 一致性1.2 Available 可用性1.3 Partition tolerance 分区容错性1.4 CAP 的矛盾1.5 CAP 的组合场景二、BASE 理论2.1 基本可用2.2 软状态2.3 最终一致性2.3.1 因果一致性2.3.2 读自身所写2.3.3 会话一致性2.3.4 单调读一致性2.3.5 单调写一…

java 教室借用管理系统_[内附完整源码和文档] 基于JAVA语言的学生选课信息管理系统...

摘 要本系统运用Java面向对象的方法设计而成。近年来&#xff0c;学生选课系统越来越在高校学生群体中得到普及&#xff0c;其所承担的功能也变得越来越丰富&#xff0c;所起到的作用也变得越来越重要&#xff0c;在被学校学生重视的同时&#xff0c;也意味着它的功能要更加完善…