热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

计算机程序的思维逻辑(45)神奇的堆

本节介绍一种神奇的数据结构-堆,应用它可以非常高效的解决很多问题,比如优先级队列、求前K个最大的元素、第K个最小的元素、求中值等,堆到底是什么?如何在堆上进行各种操作?效

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

引入堆

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

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

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

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

堆还可以实现排序,称之为堆排序,不过有比它更好的排序算法,所以,我们就不介绍其在排序中的应用了。

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

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

堆的概念

完全二叉树

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

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

技术分享
满二叉树一定是完全二叉树,但完全二叉树不要求最后一层是满的,但如果不满,则要求所有节点必须集中在最左边,从左到右是连续的,中间不能有空的。比如说,下面几个二叉树都是完全二叉树:

技术分享
而下面的这几个则都不是完全二叉树:

技术分享

编号与数组存储

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

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

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

技术分享

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

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

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

最大堆/最小堆

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

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

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

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

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

我们看下图示:

技术分享
堆概念总结

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

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

堆的算法

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

添加元素

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

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

我们来看个例子。下面是初始结构:

技术分享
添加元素3,第一步后,结构变为:

技术分享
3小于父节点8,不满足最小堆的性质,所以与父节点交换,会变为:

技术分享
交换后,3还是小于父节点6,所以继续交换,会变为:

技术分享
交换后,3还是小于父节点,也是根节点4,继续交换,变为:

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

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

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

从头部删除元素

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

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

我们来看个例子。下面是初始结构:

技术分享
执行第一步,用最后元素替换头部,会变为:

技术分享
现在根节点16小于孩子节点,与更小的孩子节点6进行替换,结构会变为:

技术分享
16还是小于孩子节点,与更小的孩子8进行交换,结构会变为:

技术分享
此时,就满足堆的性质了。

从中间删除元素

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

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

技术分享
替换后,6没有子节点,小于父节点12,执行向上调整siftup过程,最后结果为:

技术分享
我们再来看个例子,删除值为9的节点,第一步如下图所示:

技术分享
交换后,11小于右孩子10,所以执行siftdown过程,执行结束后为:

技术分享
构建初始堆

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

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

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

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

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

查找和遍历

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

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

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

算法小结

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

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

小结

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

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

但在Java中,堆到底是如何实现的呢?本文开头提到的那些问题,用堆到底如何解决呢?让我们在接下来的几节中继续探索。

---------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

技术分享

计算机程序的思维逻辑 (45) - 神奇的堆


推荐阅读
  • 二分查找算法详解与应用分析:本文深入探讨了二分查找算法的实现细节及其在实际问题中的应用。通过定义 `binary_search` 函数,详细介绍了算法的逻辑流程,包括初始化上下界、循环条件以及中间值的计算方法。此外,还讨论了该算法的时间复杂度和空间复杂度,并提供了多个应用场景示例,帮助读者更好地理解和掌握这一高效查找技术。 ... [详细]
  • 深入解析Linux内核中的进程上下文切换机制
    在现代操作系统中,进程作为核心概念之一,负责管理和分配系统资源,如CPU和内存。深入了解Linux内核中的进程上下文切换机制,需要首先明确进程与程序的区别。进程是一个动态的执行流,而程序则是静态的数据和指令集合。进程上下文切换涉及保存当前进程的状态信息,并加载下一个进程的状态,以实现多任务处理。这一过程不仅影响系统的性能,还关系到资源的有效利用。通过分析Linux内核中的具体实现,可以更好地理解其背后的原理和技术细节。 ... [详细]
  • POJ 2482 星空中的星星:利用线段树与扫描线算法解决
    在《POJ 2482 星空中的星星》问题中,通过运用线段树和扫描线算法,可以高效地解决星星在窗口内的计数问题。该方法不仅能够快速处理大规模数据,还能确保时间复杂度的最优性,适用于各种复杂的星空模拟场景。 ... [详细]
  • 该问题可能由守护进程配置不当引起,例如未识别的JVM选项或内存分配不足。建议检查并调整JVM参数,确保为对象堆预留足够的内存空间(至少1572864KB)。此外,还可以优化应用程序的内存使用,减少不必要的内存消耗。 ... [详细]
  • 本文深入探讨了Java多线程环境下的同步机制及其应用,重点介绍了`synchronized`关键字的使用方法和原理。`synchronized`关键字主要用于确保多个线程在访问共享资源时的互斥性和原子性。通过具体示例,如在一个类中使用`synchronized`修饰方法,展示了如何实现线程安全的代码块。此外,文章还讨论了`ReentrantLock`等其他同步工具的优缺点,并提供了实际应用场景中的最佳实践。 ... [详细]
  • ### 摘要`mkdir` 命令用于在指定位置创建新的目录。其基本格式为 `mkdir [选项] 目录名称`。通过该命令,用户可以在文件系统中创建一个或多个以指定名称命名的文件夹。执行此操作的用户需要具备相应的权限。此外,`mkdir` 还支持多种选项,如 `-p` 用于递归创建多级目录,确保路径中的所有层级都存在。掌握这些基本用法和选项,有助于提高在 Linux 系统中的文件管理效率。 ... [详细]
  • 本文探讨了如何有效地构建和优化微信公众平台账号,涵盖了用户信息管理、内容创作与发布、互动策略及数据分析等方面。通过合理设置用户信息字段,如用户名、昵称、密码、真实姓名和性别等,确保账号的安全性和用户体验。同时,文章还介绍了如何利用微信公众平台的各项功能,提升用户参与度和品牌影响力。 ... [详细]
  • 本文详细解析了逻辑运算符“与”(&&)和“或”(||)在编程中的应用。通过具体示例,如 `[dehua@teacher~]$[$(id -u) -eq 0] && echo "You are root" || echo "You must be root"`,展示了如何利用这些运算符进行条件判断和命令执行。此外,文章还探讨了这些运算符在不同编程语言中的实现和最佳实践,帮助读者更好地理解和运用逻辑运算符。 ... [详细]
  • 在 Android 开发中,`android:exported` 属性用于控制组件(如 Activity、Service、BroadcastReceiver 和 ContentProvider)是否可以被其他应用组件访问或与其交互。若将此属性设为 `true`,则允许外部应用调用或与之交互;反之,若设为 `false`,则仅限于同一应用内的组件进行访问。这一属性对于确保应用的安全性和隐私保护至关重要。 ... [详细]
  • 蚂蚁课堂:性能测试工具深度解析——JMeter应用与实践
    蚂蚁课堂:性能测试工具深度解析——JMeter应用与实践 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • NOIP2000的单词接龙问题与常见的成语接龙游戏有异曲同工之妙。题目要求在给定的一组单词中,从指定的起始字母开始,构建最长的“单词链”。每个单词在链中最多可出现两次。本文将详细解析该题目的解法,并分享学习过程中的心得体会。 ... [详细]
  • 在Android平台上,视频监控系统的优化与应用具有重要意义。尽管已有相关示例(如http:www.open-open.comlibviewopen1346400423609.html)展示了基本的监控功能实现,但若要提升系统的稳定性和性能,仍需进行深入研究和优化。本文探讨了如何通过改进算法、优化网络传输和增强用户界面来提高Android视频监控系统的整体效能,以满足更复杂的应用需求。 ... [详细]
  • 每日前端实战:148# 视频教程展示纯 CSS 实现按钮两侧滑入装饰元素的悬停效果
    通过点击页面右侧的“预览”按钮,您可以直接在当前页面查看效果,或点击链接进入全屏预览模式。该视频教程展示了如何使用纯 CSS 实现按钮两侧滑入装饰元素的悬停效果。视频内容具有互动性,观众可以实时调整代码并观察变化。访问以下链接体验完整效果:https://codepen.io/comehope/pen/yRyOZr。 ... [详细]
  • 优化后的标题:利用 jQuery 实现高效树形结构元素选择与操作
    在Web前端开发中,DOM结构本质上是一种树形结构。通过优化后的jQuery选择器,可以高效地选择和操作DOM树中的节点。这些选择器不仅简化了代码编写,还提高了性能和可维护性。本文将详细介绍如何利用jQuery的树形选择器实现高效的元素选择与操作。 ... [详细]
author-avatar
真心AI你fd_352
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有