《Java 编程的逻辑》笔记——第11章 堆与优先级队列(一)

本文是《Java 编程的逻辑》第11章笔记,介绍了堆的概念、算法及其在优先级队列中的应用。堆作为一种数据结构,用于实现优先级队列,能高效地处理优先级高的任务。文中详细阐述了堆的基本概念,包括完全二叉树、编号与数组存储、最大堆/最小堆,并讲解了添加元素、删除元素、构建初始堆的算法。
摘要由CSDN通过智能技术生成

声明:

本博客是本人在学习《Java 编程的逻辑》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

开头语

前面几节介绍了 Java 中的基本容器类,每个容器类背后都有一种数据结构,ArrayList 是动态数组,LinkedList 是链表,HashMap/HashSet 是哈希表,TreeMap/TreeSet 是红黑树,本节介绍另一种数据结构——堆。

引入堆

之前我们提到过堆,那里,堆指的是内存中的区域,保存动态分配的对象,与栈相对应。这里的堆是一种数据结构,与内存区域和分配无关。

堆是什么结构呢?这个我们待会再细看。我们先来说明,堆有什么用?为什么要介绍它?

堆可以非常高效方便的解决很多问题,比如说:

  • 优先级队列,我们之前介绍的队列实现类 LinkedList 是按添加顺序排队的,但现实中,经常需要按优先级来,每次都应该处理当前队列中优先级最高的,高优先级的,即使来得晚,也应该被优先处理。
  • 求前 K 个最大的元素,元素个数不确定,数据量可能很大,甚至源源不断到来,但需要知道到目前为止的最大的前 K 个元素。这个问题的变体有:求前 K 个最小的元素,求第 K 个最大的,求第 K 个最小的。
  • 求中值元素,中值不是平均值,而是排序后中间那个元素的值,同样,数据量可能很大,甚至源源不断到来。
  • 堆还可以实现排序,称之为堆排序,不过有比它更好的排序算法,所以,我们就不介绍其在排序中的应用了。

Java 容器中有一个类 PriorityQueue,就表示优先级队列,它实现了堆,下节我们会详细介绍。关于后面两个问题,它们是如何使用堆高效解决的,我们会在接下来的几节中用代码实现并详细解释。

说了这么多好处,堆到底是什么呢?

11.1 堆的概念与算法

我们先来了解堆的概念,然后介绍堆的一些主要算法。

11.1.1 基本概念

11.1.1.1 完全二叉树

堆首先是一颗二叉树,但它是完全二叉树。什么是完全二叉树呢?我们先来看另一个相似的概念,满二叉树。

满二叉树是指,除了最后一层外,每个节点都有两个孩子,而最后一层都是叶子节点,都没有孩子。比如,图 11-1 所示两个二叉树都是满二叉树。

在这里插入图片描述

满二叉树一定是完全二叉树,但完全二叉树不要求最后一层是满的,但如果不满,则要求所有节点必须集中在最左边,从左到右是连续的,中间不能有空的。比如,图 11-2 所示几个二叉树都是完全二叉树:

在这里插入图片描述

而图 11-3 所示的这几个则都不是完全二叉树:

在这里插入图片描述

11.1.1.2 编号与数组存储

在完全二叉树中,可以给每个节点一个编号,编号从 1 开始连续递增,从上到下,从左到右,如图 11-4 所示:

在这里插入图片描述

完全二叉树有一个重要的特点,给定任意一个节点,可以根据其编号直接快速计算出其父节点和孩子节点编号。如果编号为 i,则父节点编号即为 i/2,左孩子编号即为 2 * i,右孩子编号即为 2 * i + 1。比如,对于 5 号节点,父节点为 5/2 即 2,左孩子为 2 * 5 即 10,右孩子为 2 * 5 + 1 即 11。

这个特点为什么重要呢?它使得逻辑概念上的二叉树可以方便的存储到数组中,数组中的元素索引就对应节点的编号,树中的父子关系通过其索引关系隐含维持,不需要单独保持。比如说,上图中的逻辑二叉树,保存到数组中,其结构如图 11-5 所示:

在这里插入图片描述

父子关系是隐含的,比如对于第 5 个元素 13,其父节点就是第 2 个元素 15,左孩子就是第 10 个元素 7,右孩子就是第 11 个元素 4。

这种存储二叉树的方法与之前介绍的 TreeMap 是不一样的,在 TreeMap 中,有一个单独的内部类 Entry,Entry 有三个引用,分别指向父节点、左孩子、右孩子。

使用数组存储,优点是很明显的,节省空间,访问效率高。

11.1.1.3 最大堆/最小堆

堆逻辑概念上是一颗完全二叉树,而物理存储上使用数组,除了这两点,堆还有一定的顺序要求。

之前介绍过排序二叉树,排序二叉树是完全有序的,每个节点都有确定的前驱和后继,而且不能有重复元素。

与排序二叉树不同,在堆中,可以有重复元素,元素间不是完全有序的,但对于父子节点之间,有一定的顺序要求,根据顺序分为两种堆,一种是最大堆,另一种是最小堆。

最大堆是指,每个节点都不大于其父节点。这样,对每个父节点,一定不小于其所有孩子节点,而根节点就是所有节点中最大的,对每个子树,子树的根也是子树所有节点中最大的。

最小堆与最大堆正好相反,每个节点都不小于其父节点。这样,对每个父节点,一定不大于其所有孩子节点,而根节点就是所有节点中最小的,对每个子树,子树的根也是子树所有节点中最小的。

我们看图 11-6 所示:

在这里插入图片描述

11.1.1.4 堆概念总结

总结来说,逻辑概念上,堆是完全二叉树,父子节点间有特定顺序,分为最大堆和最小堆,最大堆根是最大的,最小堆根是最小的,堆使用数组进行物理存储。

这个数据结构为什么就可以高效的解决之前我们说的问题呢?在回答之前,我们需要先看下,如何在堆上进行数据的基本操作,在操作过程中,如何保持堆的属性不变。

11.1.2 堆的算法

下面,我们来看下,如何在堆上进行数据的基本操作。最大堆和最小堆的算法是类似的,我们以最小堆来说明。先来看如何添加元素。

11.1.2.1 添加元素

如果堆为空,则直接添加一个根就行了。我们假定已经有一个堆了,要在其中添加元素。基本步骤为:

  1. 添加元素到最后位置。
  2. 与父节点比较,如果大于等于父节点,则满足堆的性质,结束,否则与父节点进行交换,然后再与父节点比较和交换,直到父节点为空或者大于等于父节点。

我们来看个例子。图 11-7 是添加元素前的初始结构:

在这里插入图片描述

添加元素3,第一步后,结构如图 11-8 所示:

在这里插入图片描述

3 小于父节点 8,不满足最小堆的性质,所以与父节点交换,会变为图 11-9 所示:

在这里插入图片描述

交换后,3 还是小于父节点 6,所以继续交换,会变为图 11-10 所示:

在这里插入图片描述

交换后,3 还是小于父节点,也是根节点 4,继续交换,变为图 11-11 所示:

在这里插入图片描述

这时,调整就结束了,树保持了堆的性质。

从以上过程可以看出,添加一个元素,需要比较和交换的次数最多为树的高度,即 log2(N),N 为节点数。

这种自低向上比较、交换,使得树重新满足堆的性质的过程,我们称之为向上调整(siftup)

11.1.2.2 从头部删除元素

在队列中,一般是从头部删除元素,Java 中用堆实现优先级队列,我们来看下如何在堆中删除头部,其基本步骤为:

  1. 用最后一个元素替换头部元素,并删掉最后一个元素。
  2. 将新的头部与两个孩子节点中较小的比较,如果不大于该孩子节点,则满足堆的性质,结束,否则与较小的孩子进行交换,交换后,再与较小的孩子比较和交换,一直到没有孩子,或者不大于两个孩子节点。这个过程我们般称为向下调整(siftdown)

我们来看个例子。图 11-12 是初始结构:

在这里插入图片描述

执行第一步,用最后元素替换头部,如图 11-13 所示:

在这里插入图片描述

现在根节点 16 大于孩子节点,与更小的孩子节点 6 进行替换,结构会变为图 11-14 所示:

在这里插入图片描述

16 还是大于孩子节点,与更小的孩子 8 进行交换,结构会变为图 11-15 所示:

在这里插入图片描述

此时,就满足堆的性质了。

11.1.2.3 从中间删除元素

那如果需要从中间删除某个节点呢?与从头部删除一样,都是先用最后一个元素替换待删元素。不过替换后,有两种情况:如果该元素大于某孩子节点,则需向下调整(siftdown);否则,如果小于父节点,则需向上调整(siftup)。

我们来看个例子,删除值为 21 的节点,第一步如图 11-16 所示:

在这里插入图片描述

替换后,6 没有子节点,小于父节点 12,执行向上调整(siftup)过程,最后结果为如图 11-17 所示:

在这里插入图片描述

我们再来看个例子,删除值为 9 的节点,第一步如图 11-18 所示:

在这里插入图片描述

交换后,11 大于右孩子 10,所以执行(siftdown)过程,执行结束后为如图 11-19 所示:

在这里插入图片描述

11.1.2.4 构建初始堆

给定一个无序数组,如何使之成为一个最小堆呢?将普通无序数组变为堆的过程我们称之为 heapify

基本思路是:从最后一个非叶子节点开始,一直往前直到根,对每个节点,执行向下调整(siftdown)。换句话说,是自底向上,先使每个最小子树为堆,然后每对左右子树和其父节点合并,调整为更大的堆,因为每个子树已经为堆,所以调整就是对父节点执行(siftdown),就这样一直合并调整直到根。这个算法的伪代码是:

void heapify() {
    for (int i=size/2; i >= 1; i--)
        siftdown(i);
}

size 表示节点个数,节点编号从 1 开始,size/2 表示第一个非叶节点的编号。

这个构建的时间效率为 O(N),N 为节点个数,具体就不证明了。

11.1.2.5 查找和遍历

在堆中进行查找没有特殊的算法,就是从数组的头找到尾,效率为 O(N)。

在堆中进行遍历也是类似的,堆就是数组,堆的遍历就是数组的遍历,第一个元素是最大值或最小值,但后面的元素没有特定的顺序。

需要说明的是,如果是逐个从头部删除元素,堆可以确保输出是有序的。

11.1.2.6 算法小结

以上就是堆操作的主要算法:

  • 在添加和删除元素时,有两个关键的过程以保持堆的性质,一个是向上调整(siftup),另一个是向下调整(siftdown),它们的效率都为 O(log2(N))。由无序数组构建堆的过程 heapify 是一个自底向上循环的过程,效率为 O(N)。
  • 查找和遍历就是对数组的查找和遍历,效率为 O(N)。

11.1.3 小结

本节介绍了堆这一数据结构的基本概念和算法。

堆是一种比较神奇的数据结构,概念上是树,存储为数组,父子有特殊顺序,根是最大值/最小值,构建/添加/删除效率都很高,可以高效解决很多问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值