【数据结构篇】堆

news2024/11/28 5:40:53

文章目录

    • 前言
    • 基本介绍
      • 认识堆
      • 堆的特点
      • 堆的分类
      • 堆的操作
      • 堆的常见应用
    • 堆的实现
      • 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()); // 输出: 2
        System.out.println(minHeap.poll()); // 输出: 2
        
        System.out.println(minHeap.peek()); // 输出: 5
        System.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()); // 输出: 8
        System.out.println(maxHeap.poll()); // 输出: 8

        System.out.println(maxHeap.peek()); // 输出: 5
        System.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.coloradmin.cn/o/1052610.html

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

相关文章

No164.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

怒刷LeetCode的第20天(Java版)

目录 第一题 题目来源 题目内容 解决方法 方法一&#xff1a;回溯算法 方法二&#xff1a;permute方法 方法三&#xff1a;交换法 第二题 题目来源 题目内容 解决方法 方法一&#xff1a;回溯算法 方法二&#xff1a;递归和交换 方法三&#xff1a;二维列表 第三…

NLP 04(GRU)

一、GRU GRU (Gated Recurrent Unit)也称门控循环单元结构,它也是传统RNN的变体,同LSTM一样能够有效捕捉长序列之间的语义关联&#xff0c; 缓解梯度消失或爆炸现象&#xff0c;同时它的结构和计算要比LSTM更简单,它的核心结构可以分为两个部分去解析: 更新门、重置门 GRU的内…

苹果iPhone手机使用草柴返利APP查询领取淘宝天猫京东优惠券如何取消关闭粘贴商品链接时的弹窗提示?

使用苹果手机在淘宝或京东复制商品链接&#xff0c;到草柴APP粘贴时总是弹窗提示&#xff0c;如何关闭苹果手机粘贴弹窗的提示&#xff1f; 苹果手机如何关闭粘贴弹窗提示&#xff1f; 1、在草柴APP内&#xff0c;点击底部「我的」接着点击「系统设置」进入&#xff1b; 2、进…

【数据结构和算法】--N叉树中,返回某些目标节点到根节点的所有路径

目录 一、前言二、具体实现及拓展2.1、递归-目标节点到根节点的路径数据2.2、list转换为tree结构2.3、tree转换为list结构 一、前言 这么多年工作经历中&#xff0c;“数据结构和算法”真的是超重要&#xff0c;工作中很多业务都能抽象成某种数据结构问题。下面是项目中遇到的…

MDK报错:Undefined symbol assert_failed报错解决策略

MDK报错&#xff1a;Undefined symbol assert_failed报错解决策略 &#x1f3af;&#x1fa95;在全网搜索相关MDK编译报错:Error: L6218E: Undefined symbol assert_param (referred from xxx.o). ✨有些问题看似很简单&#xff0c;可能产生的问题是由于不经意的细节原因导致。…

16.PWM输入捕获示例程序(输入捕获模式测频率PWMI模式测频率和占空比)

目录 输入捕获相关库函数 输入捕获模式测频率 PWMI模式测频率和占空比 两个代码的接线图都一样&#xff0c;如下 测量信号的输入引脚是PA6&#xff0c;信号从PA6进来&#xff0c;待测的PWM信号也是STM32自己生成的&#xff0c;输出引脚是PA0。 需要配置电路连接图示如下&…

Grafana 开源了一款 eBPF 采集器 Beyla

eBPF 的发展如火如荼&#xff0c;在可观测性领域大放异彩&#xff0c;Grafana 近期也发布了一款 eBPF 采集器&#xff0c;可以采集服务的 RED 指标&#xff0c;本文做一个尝鲜介绍&#xff0c;让读者有个大概了解。 eBPF 基础介绍可以参考我之前的文章《eBPF Hello world》。理…

TrustRadius 评论:为什么 Splashtop 优于 LogMeIn

在当今日益数字化的格局中&#xff0c;远程访问和远程支持工具不仅方便而且至关重要。无论对于居家办公人员&#xff0c;还是对于提供远程支持的 IT 专家&#xff0c;能够安全高效地访问远程系统已成为以技术为导向的日常生活的主要内容。 Splashtop 和 LogMeIn 是远程领域的两…

FFmpeg 命令:从入门到精通 | 查看帮助文档

FFmpeg 命令&#xff1a;从入门到精通 | 查看帮助文档 FFmpeg 命令&#xff1a;从入门到精通 | 查看帮助文档FFmpeg 库模块ffmpeg 命令查看帮助文档ffplay 命令查看帮助文档ffprobe 命令查看帮助文档注意事项 FFmpeg 命令&#xff1a;从入门到精通 | 查看帮助文档 FFmpeg 库模…

【MySQL进阶】--- 存储引擎的介绍

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【MySQL学习专栏】&#x1f388; 本专栏旨在分享学习MySQL的一点学习心得&#xff0c;欢迎大家在评论区讨论&#x1f48c; 目录 一、什么…

数据集笔记:杭州地铁刷卡数据

1 数据位置&#xff1a; 链接: https://pan.baidu.com/s/1iLHomv5NRodB_3jr7FcFow 提取码: arse 全球城市计算AI挑战赛_算法大赛_天池大赛-阿里云天池的赛制 (aliyun.com) 2 数据集介绍 杭州20190101至20190125共25天地铁刷卡数据记录&#xff0c;共涉及3条线路81个地铁站约…

正态分布的概率密度函数|多种正态分布检验|Q-Q图

正态分布的概率密度函数&#xff08;Probability Density Function&#xff0c;简称PDF&#xff09;的函数取值是指在给定的正态分布参数&#xff08;均值 μ 和标准差 σ&#xff09;下&#xff0c;对于特定的随机变量取值 x&#xff0c;计算得到的概率密度值 f(x)。这个值表示…

WPF中的控件

内容控件&#xff1a;label、border Window控件 Label控件 Border控件 内容控件 Button控件 点击取消按钮关闭程序&#xff1b;点击登录按钮打开BorderWindow窗口。 TextBox控件 PasswordBox控件 TextBlock控件 加载窗口时显示TextBlock中的内容 RadioButton控件 CheckBox控件…

WPF 03

staticResource和dynamicResource的区别 首先看一个案例 MainWindow.xaml <Window x:Class"WpfDay03.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml&quo…

【教学类-40-01】A4骰子纸模制作(点数是不同的符号图案)

作品展示 背景需求 骰子是孩子们喜欢的游戏玩具&#xff0c;大3班两个孩子用纸条做了两个迷你的骰子。 所以我想在A4纸上做一个骰子的纸模。 素材准备 WORD模板 隐藏线条、设置实线、虚线、粗线等&#xff0c;慢慢调整&#xff0c;很繁琐 目的&#xff1a; 1、骰子 点数是不…

C++ 类构造函数 析构函数

类的构造函数 类的构造函数是类的一种特殊的成员函数&#xff0c;它会在每次创建类的新对象时执行。 构造函数的名称与类的名称是完全相同的&#xff0c;并且不会返回任何类型&#xff0c;也不会返回 void。构造函数可用于为某些成员变量设置初始值。 下面的实例有助于更好地…

Flink之Watermark生成策略

在Flink1.12以后,watermark默认是按固定频率周期性的产生. 在Flink1.12版本以前是有两种生成策略的: AssignerWithPeriodicWatermarks周期性生成watermarkAssignerWithPunctuatedWatermarks[已过时] 按照指定标记性事件生成watermark 新版本API内置的watermark策略 单调递增的…

Android LiveData 介绍

Android LiveData 介绍 系列文章目录前言一、LiveData是什么&#xff1f;二、简单使用依赖测试数据准备1.创建可观察的livedata2.观察它3.更新它 总结 系列文章目录 Android LiveData 介绍&#xff08;本文&#xff09; 前言 本系列根据官网介绍Jetpack中的数据通信组件&…

初识红黑树

文章目录 1.红黑树的介绍1.0红黑树的来源1.1红黑树的概念1.2红黑树的性质 2.红黑树构建示例2.1只变色1.祖父为根/不为根2.连续回溯 2.2变色旋转 3.红黑树构建情况分类3.0默认插入结点颜色3.1情况一:变色1.未知树为空2.未知树不空 3.2情况二:单旋变色1.uncle不存在2.uncle存在为…