什么是堆
堆满足下面两点:
- 是一颗完全二叉树
- 大顶堆任意孩子节点小于或等于父节点(小顶堆任意孩子节点大于或等于父节点)
先来看几张图。
上面几张图中,图1不满足完全二叉树的性质,图2是合格的最大堆,图3不满足孩子节点小于或等于父节点的性质。
堆的用途
优先队列指的是元素入队和出队的顺序与时间无关,既不是先进先出,也不是先进后出,而是根据元素的重要性来决定的。
例如,操作系统的任务执行是优先队列。一些情况下,会有新的任务进入,并且之前任务的重要性也会改变或者之前的任务被完成出队。而这个出队、入队的过程利用堆结构,时间复杂度是O($x^2$)。
注意:堆有很多种实现左式堆、斜堆、斐波那锲堆、二叉堆、三叉堆、N叉堆等。由于二叉堆使用很广泛,本文讨论的都是二叉堆。
堆的存储结构
由于数组占用的是连续的内存空间,相对来说对于散列存储的结构来说,数组可以节省连续的内存空间,不会将内存打乱,所以一般用数组存储堆的节点。
一般情况下不选用下标为0的位置来存储数据,下标为0的位置通常使用一个很大的数字或者很小的数字作为哨兵变量,从下标1开始也能更方便地表示父子节点关系。本文后续的代码实现也是从1开始存储。
堆的基本操作(Java实现)
建堆
思路是将待建堆的数组当成一颗完全二叉树,然后从最后一个非叶子节点开始,和其孩子节点比较,依次向前循环非叶子节点保证堆序性,这个过程叫做下滤(percolate down)。从其存储结构知道parent=child(i)/2,那么最后一个非叶子节点=堆的大小/2。我们定义一个建堆数组和堆的大小,如下所示:
1 | int[] arr = {-1,19,20,32,5,45,6,8,9,15,1}; //-1为哨兵变量 |
初始的完全二叉树如图6所示:
进行percolateDown(5)后如图7所示:
进行percolateDown(4)后如图8所示:
进行percolateDown(3)后如图9所示:
进行percolateDown(2)后如图10所示:
进行percolateDown(1)后如图11所示:
这样一个二叉大顶堆就建好了。下滤操作的代码如下所示:
1 | /** |
建堆操作的代码如下所示:
1 | for (int i = currentSize / 2; i >= 1; i--) |
删除堆顶元素
删除堆顶元素思路是用最后一个元素替换掉堆顶元素,堆的大小减1,再执行下滤(percolate down)操作调整。上面建好的堆删掉堆顶元素流程如下所示:
第一步,用最后一个元素替换堆,堆的大小减1。
第二步执行下滤操作,如图13-图14所示。
1 | // 用最后一个元素覆盖掉顶部的元素->堆元素个数减去1->执行下滤操作 |
插入
往堆中插入元素的思路是先把元素放到堆尾,然后循环和父节点比较大小,直到父节点比其大,这个过程叫做上滤(percolate up)。例如在上面的堆中插入50的过程如下所示:
第一步,把50放入堆尾,如图15所示。
第二步,执行上滤操作,如图16-18所示。
1 | /** |
插入操作代码如下:
1 | /** |
完整源码
1 | package com.test; |
运行结果如下所示:
建堆后结果:[-1, 45, 20, 32, 15, 19, 6, 8, 9, 5, 1]
当前堆大小:10
删除堆顶后结果:[-1, 32, 20, 8, 15, 19, 6, 1, 9, 5]
当前堆大小:9
插入50后结果:[-1, 50, 32, 8, 15, 20, 6, 1, 9, 5, 19, 0, 0, 0, 0, 0, 0, 0]
当前堆大小:10