本文目录

  • 一、二叉堆的定义

    • 结构性质
    • 堆序性质
  • 二、二叉堆的底层存储结构
  • 三、二叉堆的插入
  • 四、二叉堆的删除
  • 五、源码和测试

系列目录

  • 《树》
  • 《树的遍历》
  • 《二叉查找树》
  • 《AVL》
  • 《红黑树(上)》
  • 《红黑树(下)》
  • 《B树》
  • 《B+树》
  • 《大顶堆、小顶堆》

一、二叉堆的定义

二叉堆:首先是一棵二叉树,其次这棵二叉树要满足结构性质和堆序性质

  • 结构性质:是一颗完全二叉树
  • 堆序性质:
    • 大顶堆:对于树中的任意节点,要求key大于等于它的两个孩子,两个孩子之间没有排序要求,所以根节点拥有最大值
    • 小顶堆:对于树中的任意节点,要求key小于等于它的两个孩子,两个孩子之间没有排序要求,所以根节点拥有最小值

二叉堆常常用来实现优先级队列,在JDK的定时任务java.util.Timer中,就通过一个小顶堆TaskQueue来存放定时任务TimerTask。

1.结构性质

二叉堆是一颗完全二叉树。

完全二叉树:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

这个定义看了就像没看一样,我们讲点通俗易懂的。

满二叉树每一层的节点都是满的:

  • 第零层只有根节点,所以有2^0=1个节点
  • 第一层有2^1=2个节点
  • 第二层有2^2=4个节点
  • 第三层有2^3=8个节点
  • ......

如果一棵二叉树每一层的节点都是满的,这棵树就是满二叉树。下图就是一颗满二叉树。

满二叉树一定是一颗完全二叉树,但完全二叉树不一定是一颗满二叉树。

完全二叉树结构上很接近满二叉树,只是允许最后一层不满,并且中间不允许空洞。

下图就是一颗完全二叉树,中间没有空洞,虽然最后一层不满,但你从上到下逐层、每层从左往右的看,除了最后一层缺最后几个节点,中间一个不缺,没有一个漏洞。

像下面两图,在最后一层从左往右看的时候,你会发现中间出现了一个空洞,所以既不是满二叉树,也不是完全二叉树。

2.堆序性质

如果是大顶堆,那么对于这颗完全二叉树中的任意节点,都有该节点的key大于等于它的两个孩子,而两个孩子之间没有排序的要求。

如果是小顶堆,那么对于这颗完全二叉树中的任意节点,都有该节点的key小于等于它的两个孩子,而两个孩子之间没有排序的要求。

下图就是一个小顶堆:

这个结构虽然看起来杂乱无章,但切切实实的是一个小顶堆,稍后你看过二叉堆的源码实现你就能理解了。


二、二叉堆的底层存储结构

二叉堆的底层存储结构通常是数组,用顺序存储的数组来存放节点,并且数组的第0个下标是不用的。

所以在二叉堆中,定义一个数组用来存储插入的节点:

private Node<K, V>[] queue = new Node[size];

二叉堆的底层存储结构形如下图:

第0个下标不用,这样就很容易拿到某一个节点的父节点和孩子节点。

找到某一个节点的父节点:

 /*** 输入当前节点的index,获取父节点的index* @param index* @return*/private int parent(int index) {return index >> 1;}

找到某一个节点的左孩子:

 /*** 输入当前节点的index,获取其左孩子的index* @param index* @return*/private int left(int index) {return 1 << index;}

找到某一个节点的右孩子:

left(index) + 1;

因为要满足完全二叉树的结构性质,所以如果某一个节点只有一个孩子,那一定是左孩子,如果是右孩子那相当于左孩子处出现空洞,也就意味着二叉堆底层存放节点的数组中间出现了值为null的下标。


三、二叉堆的插入

插入操作既不能破坏二叉堆的结构性质(完全二叉树)也不能破坏二叉堆的堆序性质。

为了不破坏二叉堆完全二叉树的结构性质,我们在二叉堆中定义一个size,有两个用处:

  • 一则是用来记录堆中存放节点的数量;
  • 二则当插入节点的时候queue[++size]整好定位到新节点存放的位置,二叉堆中存放节点的数组queue[]中不会出现空洞;

为了满足二叉堆的堆序性质,新节点要不断的向上回溯:

  • 如果是大顶堆,向上回溯直至根节点或其父节点大于等于自己;
  • 如果是小顶堆,向上回溯直至根节点或其父节点小于等于自己;

大顶堆插入示例代码:

 /*** 向二叉堆中插入数据* @param key* @param value*/public void insert(K key, V value) {if ((size + 1) == this.queue.length)this.queue = Arrays.copyOf(queue, 2 * queue.length);queue[++size] = new Node<K, V>(key, value);fixup(size);}/*** 新插入的节点放入数组index=size,这样不会破坏二叉堆的结构性质(完全二叉树)* 然后拿到其父节,比较两者的key,如果新插入节点的key大于父节点的key,则两者交换位置* 这样不断向上回溯,直至跟节点或小于其父节点的key*/private void fixup(int index) {while (index > 1) {int parentIndex = parent(index);// 如果父亲的key大于等于自己的key,则停止向上回溯if (queue[parentIndex].key.compareTo(queue[index].key) >= 0)break;// 如果父亲的key小于自己的key,则与父亲交换位置,然后继续向上回溯Node<K, V> tmp = queue[parentIndex];queue[parentIndex] = queue[index];queue[index] = tmp;index = parentIndex;}}

四、二叉堆的删除

二叉堆的删除操作是摘掉根节点,即把queue[1]摘掉。

同样的,删除操作同样不能破坏二叉堆的结构性质和堆序性质。

为了保证删除操作时二叉堆完全二叉树的结构性质,当把根节点queue[1]摘掉的时候会出现空洞,于是我们把最后一个节点queue[size]挪过来填补空洞。

为了保证删除操作时二叉堆的堆序性质,当把最后一个节点queue[size]挪到根节点的时候,我们需要不断的向下回溯:

  • 如果是大顶堆,向下回溯直至两个孩子中最大的一个小于等于自己或已经没有孩子可以比较;
  • 如果是小顶堆,向下回溯直至两个孩子中最小的一个小于等于自己或已经没有孩子可以比较;

大顶堆删除示例代码:

 /*** 大顶堆的根节点拥有最大值,所以deleteMax会摘掉根节点* 这样会导致空洞,为了不破坏二叉堆完全二叉树的结构性质,将最后一个节点移动到根节点* 然后与它的两个孩子中较大的那一个作比较,如果小于则往下移动* 直至最后一层或大于其孩子中较大的那一个* @return*/public V deleteMax() {Node<K, V> root = queue[1];queue[1] = queue[size];queue[size--] = null;fixDown(1);return root.value;}private void fixDown(int index) {// 完全二叉树:如果一个节点有孩子,那一定先有左孩子int childIndex;/** 如果节点至少有一个孩子(因为是完全二叉树,所以如果有一个孩子那一定先是左孩子)* 那么进入while循环,开始向下回溯*/while ((childIndex = left(index)) <= this.size) {// 要找两个孩子中较大的那一个作比较,所以再判断一下有没有右孩子int rightIndex = childIndex + 1;if (rightIndex <= size && queue[rightIndex].key.compareTo(queue[childIndex].key) > 0)childIndex = rightIndex;// 比较较大的孩子和当前节点是否需要交换if (queue[index].key.compareTo(queue[childIndex].key) >= 0)break;Node<K, V> tmp = queue[index];queue[index] = queue[childIndex];queue[childIndex] = tmp;index = childIndex;}}

五、源码和测试

大顶堆源码:

package cn.wxy.blog2;import java.util.Arrays;/*** 大顶堆* @author 王大锤* @date 2021年6月26日*/
public class BinaryHeap<K extends Comparable<K>, V> {static class Node<K extends Comparable<K>, V> {private K key;private V value;public Node(K key, V value) {this.key = key;this.value = value;}public K getKey() {return key;}public void setKey(K key) {this.key = key;}@Overridepublic String toString() {return this.key + ":" + value + " ";}}@SuppressWarnings("unchecked")private Node<K, V>[] queue = new Node[16];private int size = 0;public BinaryHeap() {}/*** 输入当前节点的index,获取父节点的index* @param index* @return*/private int parent(int index) {return index >> 1;}/*** 输入当前节点的index,获取其左孩子的index* @param index* @return*/private int left(int index) {return 1 << index;}/*** 向二叉堆中插入数据* @param key* @param value*/public void insert(K key, V value) {if ((size + 1) == this.queue.length)this.queue = Arrays.copyOf(queue, 2 * queue.length);queue[++size] = new Node<K, V>(key, value);fixup(size);}/*** 新插入的节点放入数组index=size,这样不会破坏二叉堆的结构性质(完全二叉树)* 然后拿到其父节,比较两者的key,如果新插入节点的key大于父节点的key,则两者交换位置* 这样不断向上回溯,直至跟节点或小于其父节点的key*/private void fixup(int index) {while (index > 1) {int parentIndex = parent(index);// 如果父亲的key大于等于自己的key,则停止向上回溯if (queue[parentIndex].key.compareTo(queue[index].key) >= 0)break;// 如果父亲的key小于自己的key,则与父亲交换位置,然后继续向上回溯Node<K, V> tmp = queue[parentIndex];queue[parentIndex] = queue[index];queue[index] = tmp;index = parentIndex;}}/*** 大顶堆的根节点拥有最大值,所以deleteMax会摘掉根节点* 这样会导致空洞,为了不破坏二叉堆完全二叉树的结构性质,将最后一个节点移动到根节点* 然后与它的两个孩子中较大的那一个作比较,如果小于则往下移动* 直至最后一层或大于其孩子中较大的那一个* @return*/public V deleteMax() {Node<K, V> root = queue[1];queue[1] = queue[size];queue[size--] = null;fixDown(1);return root.value;}private void fixDown(int index) {// 完全二叉树:如果一个节点有孩子,那一定先有左孩子int childIndex;/** 如果节点至少有一个孩子(因为是完全二叉树,所以如果有一个孩子那一定先是左孩子)* 那么进入while循环,开始向下回溯*/while ((childIndex = left(index)) <= this.size) {// 要找两个孩子中较大的那一个作比较,所以再判断一下有没有右孩子int rightIndex = childIndex + 1;if (rightIndex <= size && queue[rightIndex].key.compareTo(queue[childIndex].key) > 0)childIndex = rightIndex;// 比较较大的孩子和当前节点是否需要交换if (queue[index].key.compareTo(queue[childIndex].key) >= 0)break;Node<K, V> tmp = queue[index];queue[index] = queue[childIndex];queue[childIndex] = tmp;index = childIndex;}}/*** 层序遍历二叉堆,实际上就是顺序打印二叉堆的底层数组*/public void traversalHeapByLevel() {System.out.print("打印堆queue[]:");for (Node<K, V> node : queue)System.out.print(node + " ");System.out.println();}/*** 通过删除根节点来遍历大顶堆*/public void traverssalHeapByDelete() {System.out.print("循环deleteMax:");while(size > 0)System.out.print(deleteMax() +" ");System.out.println();}
}

在大顶堆中,定义了两个方法来做测试,一个是traversalHeapByLevel方法打印大顶堆底层存储结构queue[],一个是traverssalHeapByDelete不断的deleteMax摘掉大顶堆的根节点,其中traverssalHeapByDelete的输出应该满足从大到小的顺序。

测试代码:

 public static void main(String[] args) {int[] array = {8, 57, 11, 34, 53, 71, 87, 21, 98, 81};printArray(array);BinaryHeap<Integer, String> heap = new BinaryHeap<Integer, String>();for (int i : array)heap.insert(i, i + "");heap.traversalHeapByLevel();heap.traverssalHeapByDelete();}public static void printArray(int[] array) {System.out.print("测试数据:");Arrays.stream(array).forEach(value -> System.out.print(value + " "));System.out.println();}

输出结果:

测试数据:8 57 11 34 53 71 87 21 98 81 
打印堆queue[]:null 98:98  87:87  71:71  53:53  81:81  11:11  57:57  8:8  21:21  34:34  null null null null null 
循环deleteMax:98 87 81 71 57 53 34 21 11 8


参考资料:

  • 《算法导论》
  • 《数据结构与算法:Java语言描述》
  • 《大话数据结构》
  • jdk java.util.TaskQueue源码

【tree】二叉堆(大顶堆或小顶堆)相关推荐

  1. 【基础知识】 之 Binary Search Tree 二叉搜索树

    前言 这个系列是毕业找工作的复习笔记,希望可以和广大正准备毕业的童鞋一起打牢基础,迎接各种笔试--为了应付中英文笔试,关键词都用英文进行标注,这样就不怕面对英文题目了.之所以开始这一系列是因为之前在参 ...

  2. PAT甲级——1099 Build A Binary Search Tree (二叉搜索树)

    本文同步发布在CSDN:https://blog.csdn.net/weixin_44385565/article/details/90701125 1099 Build A Binary Searc ...

  3. [leetcode] 230. Kth Smallest Element in a BST 找出二叉搜索树中的第k小的元素

    题目大意 https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/ 230. Kth Smallest Elem ...

  4. LeetCode#230.二叉搜索书中第k小的元素

    二叉搜索树种第k小的元素:给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素. 三种解法: 1.第一种是要计算左子树的节点个数,然后来判断第k个节点在根还是左子树 ...

  5. 图解:什么是二叉堆?

    在正式开始学习堆之前,一定要大脑里回顾一下什么是完全二叉树,因为它和堆可是息息相关奥! 如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树. 而如果二叉树中除去最后一层节点为满二叉 ...

  6. Java PriorityQueue(优先级队列/二叉堆)的使用及题目应用

    目录 PriorityQueue有几个需要注意的点: 重写比较器的方法 应用题目 LeetCode 1845. 座位预约管理系统 LeetCode 215. 数组中的第 K 个最大元素(同剑指 Off ...

  7. 42. 盘点那些必问的数据结构算法题之二叉堆

    盘点那些必问的数据结构算法题之二叉堆 0 概述 1 二叉堆定义 2 保持堆的性质 3 建立最大堆 4 堆排序 5 优先级队列 参考资料 0 概述 本文要描述的堆是二叉堆.二叉堆是一种数组对象,可以被视 ...

  8. 剑指Offer之寻找数据流中的中位数【包含大顶堆小顶堆解释】

    数据流中的中位数 题目描述 题解 最小堆和最大堆解释 参考链接 题目描述 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值.如果从数据流中读出偶 ...

  9. 大顶堆小顶堆java_《排序算法》——堆排序(大顶堆,小顶堆,Java)

    十大算法之堆排序:堆的定义例如以下: n个元素的序列{k0,k1,...,ki,-,k(n-1)}当且仅当满足下关系时,称之为堆. " ki<=k2i,ki<=k2i+1;或ki ...

  10. 大顶堆,小顶堆——排序问题

    如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值.如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值. 例如, [2 ...

最新文章

  1. webpack热更新实现
  2. 强化学习(三)用动态规划(DP)求解
  3. JIRA7.10迁移
  4. cocos2d-x打包后手机运行闪退_三国志11手机版,问题解决手册 1.4.4版本
  5. 在.net中加载dll的一种错误问题原因及处理
  6. 蓝桥杯java能用编译器1吗_学java的你,这些英文单词都掌握了吗?
  7. js读取服务器上的txt文件,javascript – 每15秒读取一次文本文件的内容
  8. ubuntu mysql 操作_Ubuntu系统下MySQL数据库基本操作
  9. SIFT特征原理与理解
  10. 电脑重启只剩下c盘怎么办_win10突然只剩下c盘了怎么办|win10突然只剩下c盘的解决方法...
  11. java 数组和集合的区别
  12. 一文搞懂 | Linux 同步管理(上)
  13. 繁星花落谁家(屠龙)算法和统计概率结论
  14. Java中的Enum的简单使用
  15. 三种振幅调制AM、DSB、SSB
  16. 首次曝光:大厂都是这样过1024的,看的我酸了
  17. 资产初探:理财直接融资工具
  18. 工作三年程序员收入到底多高?透露收入:网友:哇,真的好高呀!
  19. mysql水果销售系统数据库_mysql数据库水果销售系统
  20. php 卡路里计算,那些每天计算卡路里的人,为什么永远也瘦不下来?

热门文章

  1. beckhoff倍福TwinCAT HMI使用笔记,BestMrRight整理
  2. 数据库设计范式及数据冗余存储
  3. wordpress需要FTP用户名密码的问题
  4. 山东企业CE认证流程
  5. 课堂讨论:软件改变世界
  6. #systemverilog# 之 event region 和 timeslot 仿真调度(二)
  7. Jaocibian(雅可比)矩阵本质上就是导数
  8. 致敬Evi,UNIX/Linux 系统管理技术手册第5版
  9. 卸载亚信科技安全助手
  10. python与医学图像处理