数据结构——堆(带图详解)

2023-11-18

目录

堆 

堆的概念

堆的性质

堆的创建

1、堆向下调整

2、堆的创建

3、建堆的时间复杂度

堆的插入和删除

1、堆的插入

2、堆的删除

堆的应用

1、优先级队列的实现

2、堆排序

3、Top-k问题 


 

堆 (Heap)

堆的概念

前面介绍的优先级队列在JDK1.8中其底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调整。

如果有一个 关键码的集合 K = {k0 k1 k2 kn-1} ,把它的所有元素 按完全二叉树的顺序存储方式存储 在一 个一维数组中 ,并满足: Ki <= K2i+1 Ki<= K2i+2 (Ki >= K2i+1 Ki >=K2i+2) i = 0 1 2… ,则 称为小堆 ( 或大堆) 。(即双亲比孩子的数值小(大)——小(大)堆)将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

 

 

下面来看一下堆的可视化操作堆的可视化操作icon-default.png?t=LA92https://visualgo.net/zh/heap

堆的创建

1、堆向下调整

对于集合 { 27,15,19,18,28,34,65,49,25,37 } 中的数据,如果将其创建成堆呢?
仔细观察上图后发现: 根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可
向下过程 ( 以小堆为例 )
1. parent 标记需要调整的节点, child 标记 parent 的左孩子 ( 注意: parent 如果有孩子一定先是有左孩子 )
2. 如果 parent 的左孩子存在,即 :child < size , 进行以下操作,直到 parent 的左孩子不存在
  • parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记
  • parent与较小的孩子child比较,如果:parent小于较小的孩子child,调整结束。否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = childchild = parent*2+1; 然后继续2

public void shiftDown(int[] array, int parent) {
    // child先标记parent的左孩子,因为parent可能右左没有右
    int child = 2 * parent + 1;
    int size = array.length;
    while (child < size) {
           // 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
           if(child+1 < size && array[child+1] < array[child]){
                 child += 1;
           }
 
           // 如果双亲比其最小的孩子还小,说明该结构已经满足堆的特性了
           if (array[parent] <= array[child]) {
                 break;
           }else{
                 // 将双亲与较小的孩子交换
                 int t = array[parent];
                 array[parent] = array[child];
                 array[child] = t;
 
                 // parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
                 parent = child;
                 child = parent * 2 + 1;
           }
    }
}
注意:在调整以 parent 为根的二叉树时,必须要满足 parent 的左子树和右子树已经是堆了才可以向下调整。
时间复杂度分析:
最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(log₂N)

2、堆的创建

那对于普通的序列 { 1,5,3,8,7,6 } ,即根节点的左右子树不满足堆的特性,又该如何调整呢?
public static void createHeap(int[] array) {
    // 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
    for(int root = (array.length-2)/2; root >= 0; root--){
            shiftDown(array, array.length, root);
    }
}

3、建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明 ( 时间复杂度本来看的就是近似值,多几个节点不影响最终结果)

因此:建堆的时间复杂度为O(N) 

堆的插入和删除

1、堆的插入

堆的插入总共需要两个步骤:
  1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
  2. 将最后新插入的节点向上调整,直到满足堆的性质

 

public void shiftUp(int child) {
    // 找到child的双亲
    int parent = (child - 1) / 2;
 
    while (child > 0) {
    // 如果双亲比孩子大,parent满足堆的性质,调整结束
          if (array[parent] > array[child]) {
              break;
          }else{
          // 将双亲与孩子节点进行交换
          int t = array[parent];
          array[parent] = array[child];
          array[child] = t;
 
          // 小的元素向下移动,可能到值子树不满足对的性质,因此需要继续向上调增
          child = parent;
          parent = (child - 1) / 2;
          }
    }
}

2、堆的删除

堆的删除一定删除的是堆顶元素。
堆的删除步骤如下:
  1. 将堆顶元素对堆中最后一个元素交换
  2. 将堆中有效数据个数减少一个
  3. 对堆顶元素进行向下调整
public static void shiftDown(int[] array, int size, int parent){
        int child = parent*2+1;

        while(child < size){
            // 找左右孩子中较大的孩子
            if(child+1 < size && array[child+1] > array[child]){
                child += 1;
            }

            // 双亲小于交大的孩子
            if(array[parent] < array[child]){
                swap(array, parent, child);
                parent = child;
                child = parent*2+1;
            }else{
                return;
            }
        }
}

堆的应用

1、优先级队列的实现

用堆作为底层结构 封装优先级队列
public class MyPriorityQueue {
    Integer[] array;
    int size;   // 有效元素的个数

    public MyPriorityQueue(){
        array = new Integer[11];
        size = 0;
    }

    public MyPriorityQueue(int initCapacity){
        if(initCapacity < 1){
            throw new IllegalArgumentException("初始容量小于1");
        }

        array = new Integer[initCapacity];
        size = 0;
    }

    public MyPriorityQueue(Integer[] arr){
        // 1. 将arr中的元素拷贝到数组中
        array = new Integer[arr.length];
        for(int i = 0; i < arr.length; ++i){
            array[i] = arr[i];
        }
        size = arr.length;

        // 2. 找当前完全二叉树中倒数第一个叶子节点
        //    注意:倒数第一个叶子节点刚好是最后一个节点的双亲
        //    最后一个节点的编号size-1  倒数第一个非叶子节点的下标为(size-1-1)/2
        int lastLeafParent = (size-2)/2;

        // 3. 从倒数第一个叶子节点位置开始,一直到根节点的位置,使用向下调整
        for(int root = lastLeafParent; root >= 0; root--){
            shiftDown(root);
        }
    }

    boolean offer(Integer e){
        if(e == null){
            throw new NullPointerException("插入时候元素为null");
        }

        ensureCapacity();

        array[size++] = e;

        // 注意:当新元素插入之后,可能会破坏对的性质---需要向上调整
        shiftUp(size-1);
        return true;
    }

    // 将堆顶的元素删除掉
    public Integer poll(){
        if(isEmpty()){
            return null;
        }

        Integer ret = array[0];

        // 1. 将堆顶元素与堆中最后一个元素交换
        swap(0, size-1);

        // 2. 将堆中有效元素个数减少一个
        size--;  // size -= 1;

        // 3. 将堆顶元素往下调整到合适位置
        shiftDown(0);
        return ret;
    }

    public int size(){
        return size;
    }

    public boolean isEmpty(){
        return size == 0;
    }

    public void clear(){
        size = 0;
    }

    // 功能:调整以parent为根的二叉树
    //    前提:必须要保证parent的左右子树已经满足堆的特性
    // 时间复杂度:O(logN)
    private void shiftDown(int parent){
        // 默认让child先标记左孩子---因为:parent可能有左没有右
        int child = parent*2 + 1;

        // while循环条件可以保证:parent的左孩子一定存在
        //       但是不能保证parent的右孩子是否存在
        while(child < size){
            // 1. 找到左右孩子中较小的孩子
            if(child+1 < size && array[child+1] < array[child]){
                child += 1;
            }

            // 2. 较小的孩子已经找到了
            //    检测双亲和孩子间是否满足堆的特性
            if(array[parent] > array[child]){
                swap(parent, child);

                // 大的双亲往下走了,可能会导致子树又不满足堆的特性
                // 因此需要继续往下调整
                parent = child;
                child = parent*2 + 1;
            }else{
                // 以parent为根的二叉树已经是堆了
                return;
            }
        }
    }

    private void shiftUp(int child){
        int parent = (child-1)/2;

        while(child != 0){
            if(array[child] < array[parent]){
                swap(child, parent);
                child = parent;
                parent = (child-1)/2;
            }else{
                return;
            }
        }
    }

    private void ensureCapacity(){
        if(array.length == size){
            int newCapacity = array.length*2;
            array = Arrays.copyOf(array, newCapacity);
        }
    }

    // 注意:left和right是数组的下标
    private void swap(int left, int right){
        int temp = array[left];
        array[left] = array[right];
        array[right] = temp;
    }

2、堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
  • 升序:建大堆
  • 降序:建小堆
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序
public static void swap(int[] array, int left, int right){
        int temp = array[left];
        array[left] = array[right];
        array[right] = temp;
    }

    public static void shiftDown(int[] array, int size, int parent){
        int child = parent*2+1;

        while(child < size){
            // 找左右孩子中较大的孩子
            if(child+1 < size && array[child+1] > array[child]){
                child += 1;
            }

            // 双亲小于交大的孩子
            if(array[parent] < array[child]){
                swap(array, parent, child);
                parent = child;
                child = parent*2+1;
            }else{
                return;
            }
        }
    }

    // 假设:升序
    public static void heapSort(int[] array){
        // 1. 建堆----升序:大堆    降序:小堆---向下调整
        for(int root = (array.length-2)/2; root >= 0; root--){
            shiftDown(array, array.length, root);
        }


        // 2. 利用堆删除的思想来排序---向下调整
        int end = array.length-1;   // end标记最后一个元素
        while(end != 0){
            swap(array,0,end);
            shiftDown(array, end,0);
            end--;
        }
    }

3、Top-k问题 

TOP-K 问题:即求数据结合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大
对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能数据都不能一下子全部加载到内存中) 。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前 K 个元素来建堆
  • k个最大的元素,则建小堆
  • k个最小的元素,则建大堆
2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。

Top-k问题

class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] vec = new int[k];
        if (k == 0) { // 排除 0 的情况
            return vec;
        }
        PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>() {
            public int compare(Integer num1, Integer num2) {
                return num2 - num1;
            }
        });
        for (int i = 0; i < k; ++i) {
            queue.offer(arr[i]);
        }
        for (int i = k; i < arr.length; ++i) {
            if (queue.peek() > arr[i]) {
                queue.poll();
                queue.offer(arr[i]);
            }
        }
        for (int i = 0; i < k; ++i) {
            vec[i] = queue.poll();
        }
        return vec;
    }
}

复杂度分析

时间复杂度O(nlog k),其中 n 是数组 arr 的长度。由于大根堆实时维护前 k 小值,所以插入删除都是O(logk) 的时间复杂度,最坏情况下数组里 n 个数都会插入,所以一共需要 O(nlogk) 的时间复杂度。

空间复杂度O(k),因为大根堆里最多 k 个数

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

数据结构——堆(带图详解) 的相关文章

  • 算法学习(四)查找问题

    一 查找问题通常有2类 1 查找有无 元素a是否存在 set 集合 2 查找对应关系 键值对应 元素a出现了几次 map 字典 leetcode349 两个数组的交集 给定两个数组 编写一个函数来计算它们的交集 输出结果中的每个元素一定是唯
  • 11-矩阵(matrix)_方阵_对称阵_单位阵_对角阵

    矩阵 向量是对数的拓展 一个向量表示一组数 矩阵是对向量的拓展 一个矩阵表示一组向量 1 2
  • win10+pytorch(gpu)下载安装中踩的坑,下载安装全流程

    整个下载安装的流程如下 1 查看自己的电脑显卡是否支持gpu 具体查看方式可以参考我的这一篇文章 CUDA cuDNN下载安装 配备GPU环境 九九19496的博客 CSDN博客 但先不要下载cuda和cudnn 2 确定自己想要的pyto
  • TCP流式服务的粘包问题及解决方法

    TCP流式服务的粘包问题 有可能将两次send的内容合并在一起被接受端收到 解决方法 发送定长包 包层加入 r n标记 FTP协议就是这么做的 但这种方案存在的问题就是 如果数据正文存在同样的字符 就会被误判为消息的边界 包头加上包体长度

随机推荐

  • requery与ajax,总结一下query中ajax的几种方法

    1 a中比需抖接朋功要朋插jax ajax type POST 提交数据的类型 POST GET url testLogin aspx 提交的网址 提交的数据 data Name sanmao Password sanmaoword 返回数
  • 职高计算机班主任工作计划,教学工作计划:高职班主任工作计划

    作为高职院校各专业的班主任 面临着教学理念和班级管理双重压力 高职班主任工作计划不仅要从学生学习上进行合理计划 更要从学生思想教育上进行计划 以下是小编整理的高职班主任工作计划 欢迎大家参阅 高职班主任工作计划 装潢专业 经过一年半的锻炼与
  • java对list集合进行分页

    1 计算页数 List
  • CAN总线之错误检测以及错误状态简介

    CAN总线之错误检测以及错误状态简介 1 CAN错误检测特点简介 1 1错误检测机制 2 错误 2 1错误状态的种类 本文参考瑞萨的 CAN总线入门 周立功的 现场总线CANopen设计与应用 1 CAN错误检测特点简介 错误检测是CAN的
  • java发邮件

    1 设置邮箱smtp服务 获取第三方授权码 mailHost smtp 163 com mailFrom xxx 163 com mailUser xxx mailPassword xxxpassword 主题 StringBuffer s
  • 本土化的ChromeOS-系统推荐

    系统推荐 今天推荐一个本土化的ChromeOS 可以安装安卓软件和Play商店的软件 不需要拥有谷歌账号即可使用 只需要创建fydeOS的账号就可以了 相比起其它的第三方ChromeOS操作系统 它开放的东西更多 官方也有适配一些第三方设备
  • STM32速成笔记—Flash闪存

    文章目录 一 Flash简介 二 STM32F1的Flash 三 Flash操作步骤 四 程序设计 4 1 读取数据 4 2 写入数据 不检查 4 3 写入数据 检查 五 注意事项 一 Flash简介 快闪存储器 flash memory
  • 2023前端面试题及答案整理(JS面试题)

    ES6 let const 全局作用域中 用 const 和 let 声明的变量不在 window 上 那到底在哪里 如何去获取 ES6规定 var 命令和 function 命令声明的全局变量 依旧是顶层对象的属性 但 let const
  • Python数据结构与算法分析 第五章 搜索和排序

    有序列表的顺序搜索 二分查找 二分搜索 sou 17 18 22 28 38 78 89 99 100 def binarysearch list item first 0 last len list 1 while first lt la
  • linux环境下网卡重启

    网卡重启命令 sudo service network restart 但在有的linux版本中会出现network unrecognized service 我们可以换个网络重启命令就可以了 sudo service network ma
  • 100天精通Python(基础篇)——第5天:基础语句

    作者介绍 Python领域优质创作者 数据开发工程师 励志成为Python全栈工程师 关注我发现更多精彩 本文已收录于Python全栈系列专栏 100天精通Python从入门到就业 欢迎订阅 订阅后可私聊进Python全栈VIP交流群 手把
  • 激发创造力!如何轻松录制PPT和人像视频

    大家好 作为一个追求创意的人 你是否曾经遇到过想要一边讲PPT一边录制视频 或者同时录制PPT和人像的困扰 别担心 我来给你分享一些简单而有趣的方法 首先 我们可以利用PPT自带的屏幕录制功能来实现一边讲PPT一边录制视频的需求 只需点击
  • Python运算符列表

    Python运算符列表 运算符 描述 x y x y 加 减 号可重载为连接符 x y x y x y x y 相乘 求平方 相除 求余 号可重载为重复 号可重载为格式化 lt lt gt gt lt gt 比较运算符 lt lt gt g
  • 亚马逊秋季促销指南——如何更好的利用促销?

    最新消息 亚马逊官方宣布将会在10月份举行Prime会员大促 覆盖多个站点 亚马逊卖家们一定要抓住这波促销机会 在这个秋季再冲一把 但是还有一些小白玩家可能对于亚马逊促销了解不够 那么接下来我要讲的这些准备工作 你要看完哦 一 促销工具 首
  • TensorFlow的随机张量(Random Tensors)

    随机张量 Random Tensors TensorFlow有几种操作可以创建具有不同分布的随机张量 随机操作是有状态的 每次评估时都会创建新的随机值 与变量不同 随机张量在运行前不再需要显式初始化 tf random normal tf
  • flink-connector-jdbc_2.12 简介、中文文档、中英对照文档 下载

    flink connector jdbc 2 12 文档 下载链接 含jar包 源码 pom 组件名称 中文 文档 下载链接 中英对照 文档 下载链接 flink connector jdbc 2 12 1 14 3 jar flink c
  • Oracle数据库管理-ORA-12542 TNS 地址已被占用

    今日客户数据库连接报错 在使用plsql进行数据库连接时出现如下报错信息 ORA 12542 TNS 地址已被占用 问题排查 1 使用其他服务器客户端连接数据库正常 2 只有这一台机器连接数据库异常 查询相关的metalink文档发现如下
  • 原生input实现上传文件以及文件夹

    最近接到了一个上传文件和文件夹的需求 考虑了一下打算用原生的input来实现上传文件 话不多说直接上代码 1 html部分
  • NLP之基于TextCNN的文本情感分类

    TextCNN 文章目录 TextCNN 1 理论 1 1 基础概念 最大汇聚 池化 层 请添加图片描述 https img blog csdnimg cn 10e6e1ed6bfd42f0bd46b658ed52ff50 png 1 2
  • 数据结构——堆(带图详解)

    目录 堆 堆的概念 堆的性质 堆的创建 1 堆向下调整 2 堆的创建 3 建堆的时间复杂度 堆的插入和删除 1 堆的插入 2 堆的删除 堆的应用 1 优先级队列的实现 2 堆排序 3 Top k问题 堆 Heap 堆的概念 前面介绍的优先级