目录

堆的概念

堆的性质

堆的创建

1、堆向下调整

2、堆的创建

3、建堆的时间复杂度

堆的插入和删除

1、堆的插入

2、堆的删除

堆的应用

1、优先级队列的实现

2、堆排序

3、Top-k问题


堆 (Heap)

堆的概念

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

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

堆的性质

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

下面来看一下堆的可视化操作堆的可视化操作https://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 = child;child = 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)/2int 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 个数

数据结构——堆(带图详解)相关推荐

  1. VMware里建立虚拟机快照(带图详解)

    VMware里建立虚拟机快照(带图详解) 创建快照 如下图所示,想要创建快照有两种方法 可以自行修改名称和描述,修改好之后电机"拍摄快照"即可 此时大家可以看一下自己的快照是否建立 ...

  2. 【数据结构】动图详解二叉树——堆及堆排序

    目录 一.树的概念 1.树的特征 2.树的相关名词 二.树的表示方法 1.左孩子右兄弟表示法 2.双亲表示法 三.特殊二叉树 四.堆的向上调整算法建堆及排序 1.向上调整建堆O(N*logN) 2.向 ...

  3. 数据结构:二叉树(带图详解)

    目录 树的概念和结构 树的概念 树的表示形式 二叉树 二叉树的概念 两种特殊的二叉树 1.满二叉树 2.完全二叉树 二叉树的性质 二叉树的存储 二叉树的遍历 1. 前中后序遍历 还原二叉树 2.层序遍 ...

  4. Iconfont矢量图图标怎么使用带图详解

    我们经常在网上看到的一些好看的小图标,很多其实它不是img.png图片,而是矢量图.那什么是矢量图呢?先简单普及一下: 计算机中显示的图形一般可以分为两大类--矢量图和位图. 矢量图使用直线和曲线来描 ...

  5. Dijkstra最短路径算法C++带图详解

    一.问题定义 求解单元点的最短路径问题:给定带权有向图G和源点v,求v到G中其他顶点的最短路径 限制条件:图G中不存在负权值的边 二.思想 划重点,迪杰斯特拉最最朴素的思想就是按长度递增的次序产生最短 ...

  6. uniapp微信授权+获取手机号+解密手机号(带图详解)

    那废话不多说,开始今天的教程,我会在尾部添加全部代码,如果你懒的自己写,可以复制我的代码,但是里面的一些数据你需要更换,我会标注出来 1.获取微信信息(我的写法是自动获取,而不需要点击触发事件获取). ...

  7. 广度优先搜索算法带图详解

    1.前言 广度优先搜索https://so.csdn.net/so/search?q=%E5%B9%BF%E5%BA%A6%E4%BC%98%E5%85%88%E6%90%9C%E7%B4%A2&am ...

  8. Linux常用基础指令、Linux常用工具(软件包)使用带图详解

    目录 目录相关指令: 文件相关指令: 压缩解压缩指令: 匹配查找指令: 权限相关指令: Linux常用工具: 指令使用规则:指令 [该指令的详细操作选项] [操作对象(通常有路径)],后面两可不加. ...

  9. ibd文件结构组成(带图详解)

    ibd文件结构组成 页数据格式 InnodDB采用Btree作为存储结构,当用户创建一个Table的时候,就会根据显示或隐式定义的主键构建了一棵Btree,而构成Btree的叶子节点被称为Page,默 ...

最新文章

  1. syntax error near unexpected token
  2. Solaris ALOM1.6 SC Password Reset
  3. Linux入门基础思维导图
  4. 《短文本数据理解(1)》一1.3 短文本理解框架
  5. mysql 集群 qps_MySQL Cluster:如何通过扩展为MySQL带来2亿QPS
  6. 无法在只读编辑器中编辑_Mol Plant中国农科院作物科学研究所夏兰琴课题组成功利用 优化的引导基因编辑器在水稻中实现高效精准基因编辑...
  7. KP-ABE基于属性的加密加解密算法及Access Tree构建
  8. python日期对照表_2020年日期表-python实现
  9. mysql分组查询 having,MYSQL-分组查询-where和having的区别
  10. git安装 苹果笔记本_自己挖的坑自己填,无光驱安装苹果笔记本双系统
  11. 2020法研杯比赛阅读理解任务冠军参赛总结
  12. SaaS应用架构师所面临的最大挑战
  13. Eclipse配置反编译问题
  14. 前端学习(六)——HTML5中通过CSS设置超链接及鼠标形状
  15. oracle集群如何搭建,Oracle集群搭建步骤.docx
  16. 软件工程—01可行性研究报告
  17. 学习之苦也正是学习之甜------知识的本质
  18. Windows 7 安装VS2008 SP1 失败 1
  19. static 函数和变量
  20. linux中Swap分区是做什么的?

热门文章

  1. Linux学习笔记之callback
  2. mysql数据库索引的创建语句
  3. python实现二分查找代码+详解
  4. ISOMAP函数官方代码-matlab
  5. Java学习笔记(笔记来源:B站UP主遇见狂神说的Java课程总结)
  6. Mac升级系统后SecureCRT意外退出
  7. html代码重排,是什么导致重排(layout / reflow)
  8. hosts配置了域名,但是不能访问的解决办法
  9. 卓盟科技: 动态资源加载技术背后的兼容性升级 | Android 开发者故事
  10. RTSP协议 (中文协议定义)