堆结构是一种非常重要的基础数据结构,也是算法的重要内容,很多题目甚至只能用堆来进行,所以我们必须先明确什么类型的题目可以用堆,以及如何使用堆来解决。由于堆的构造和维护过程都非常复杂,因此面试时一般不需要手写堆的实现过程,但是java、python、C++已经提供了一些工具,因此需要知道思路就可以。
本关,我们主要介绍堆如何增删改查的,不用管代码怎么写,后面我们再介绍如何使用堆来解决问题
关卡名 | 堆结构 | 我会了✔️ |
内容 | 1.理解堆是如何构造的 | ✔️ |
2.理解堆是如何添加元素的 | ✔️ | |
3.掌握堆是如何删除元素的 | ✔️ |
1.堆的概念与特征
堆是将一组数据按照完全二叉树的存储顺序,将数据存储在一个一维数组中的结构。堆有两种结构,一种称为大顶堆,一种称为小顶堆,如下图。
- 小顶堆:任意节点的值均小于等于它的左右孩子,并且最小的值位于堆顶,即根节点处。
- 大顶堆:任意节点的值均大于等于它的左右孩子,并且最大的值位于堆顶,即根节点处。 有些地方也叫大根堆、小根堆,或者最大堆、最小堆都一个意思。大和小的特征等都是类似的,只是比较的时候是按照大还是小来定,我们本章在原理方面的介绍就按照最大堆来进行,后面的题目再根据情况来定。
既然是将一组数据按照树的结构存储在一维数组中,而且还是完全二叉树,那么父子之间关系的建立就很重要了。
有个概念需要注意一下,我们在做题时经常会看到有些地方叫堆,有些地方叫优先级队列,两者到底啥关系呢?
优先队列:说到底还是一种队列,他的工作就是poll()/peek()出队列中最大/最小的那个元素,所以叫带有优先级的队列。能够实现优先功能的策略不一定只有堆,例如二项堆、平衡树、线段树、C++里会用二进制分组的vector来实现一个优先队列。
堆:堆是一个很大的概念 他并不一定是完全二叉树。我们之前用完全二叉树是因为这个很容易被数组储存,但是除了这种二叉堆之外,我们还有二项堆、斐波那契堆、这种堆就不属于二叉树。
所以说,优先队列和堆不是一个同level的概念 ,但是java的PriorityQueue就是堆实现的,因此在java领域可以认为堆就是优先级队列,优先级队列就是堆,换做其他场景则不行。
2 堆的构造过程
使用数组构建堆时,就是先按照层次将所有元素依次填入二叉树中,使其成为二叉树,然后再不断调整,最终使其符合堆结构。
这里先假设一个节点的下标为i:
1、当i = 0时,为根节点。
2、当i>=1时,父节点为(i - 1)/2。
size就是元素的个数,从1开始计数。
下面就看一下如何建立一个大堆:
将元素依次排到完全二叉树节点上去,如下左图所示。
- int i = (size - 2)/2 = 4(思考一下这里为什么是size-2而不是size-1)。找到数组中的4号下标。65大于其孩子,满足大堆性质,所以不用交换。如下右图
- 然后i= i-1;然后用2和其孩子比较,2和204交换。交换之后204所在的子树满足大堆,如下左图。
- 54和其孩子比较,54和92交换。此时92所在子树满足大堆,如下右图。
- 继续,23和其孩子比较,23和204交换,交换完之后,23的子树却不满足了,所以还需调整它的子树。 如下两图所示。
- 12和204交换,仍然出现不平衡的情况,以此类推,直到根节点也满足要求就完毕了。
这样我们就建好了一个大顶堆,从图中可以看到,根元素是整个树中值最大的那个,而第二大和第三大就是其左右子树,具体是哪个更大则是未知的,需要比较一下才知道。
另外,对于同一组数据,如果输入的序列不一样,那最终构造的树是否也会不一样呢?非常有可能,那这样的树有什么意义呢?我们后面再看,这里先理解堆是这么构建的就行了。
3 插入操作
从上面可以看到根节点和其左右子节点是堆里的老大老二和老三,其他结点则没有太明显的规律,那如果要插入一个新元素,该怎么做呢?直接说规则,将元素插入到保持其为完全二叉树的最后一个位置,然后顺着这条支路一直向上调整,每前进一层就要保证其子树都满足堆否则就去处理子树,直到完全满足要求。
看一个例子,如下图,要插入300, 我们将其插入到31的右孩子位置,然后不断向上爬,31<300,所以两者要交换,再向上发现300比65大,所以两者要交换。最后300比根元素204大,两者也交换。最后就得到了新的堆。完整过程如下所示:
4 删除操作
堆本身比较特殊,一般对堆中的数据进行操作都是针对堆顶的元素,即每次都从堆中获得最大值或最小值,其他的不关心,所以我们删除的时候,也是删除堆顶。如果直接删掉堆顶,整个结构被破坏了,群龙无首就不易管理了。所以实际策略是先将堆中最后一个元素(假如为A)和堆顶元素进行替换,然后删除堆中最后一个元素。之后再从根开始逐步与左右比较,谁更大谁上位。然后A再继续与子树比较,如果有更大的继续交换,直到自己所在的子树也满足大顶堆。
上面的过程可以理解为皇上突然驾崩了,这时候先找个顾命大臣维持局面,大臣先看左右两个皇子谁更强谁就是老大。然后大臣自己再逐步隐退,直到找到属于自己的位置。
最后新的堆结构如下:
说了这么多,你觉得这东西的价值在哪里呢?价值就在于大顶堆的根节点是整个树最大的那个,增加时会根据根的大小来决定要不要加,而删除操作只删除根元素。这个特征可以在很多场景下有奇妙的应用,后面的算法题全都基于这一点。
这里可能有些人还有疑问,感觉不管插入还是删除,堆的操作都不简单,那为什么还说堆的效率比较高呢?
这是因为堆元素的数量是有限制的,一般不用将所有的元素都放到堆里。后面题目中可以看到,在序列中找K大,则堆的大小就是K。如果K个链表合并,那么堆就是K。原理后面详细展开。
说了这么多堆的性质,我们来看一下堆到底怎么解决问题的。关于堆的问题,记住口诀:
查找:找大用小,大的进;找小用大,小的进。
排序:升序用小,降序用大。
查找的口诀解释一下就是:是找K大,则用小堆,后续数据只有比根元素更大时才允许进入堆。如果是找K小,则对应反过来。后面我们结合例子分析为什么。