扫码关注公众号,获取更多内容
目录
一、什么是堆
堆是一种特殊的二叉树,它满足如下两个条件:
1、堆是一个完全二叉树(除了最后一层,其他层的节点个数都是满的,最后一层的子节点都靠左排列)。
2、堆中每一个节点的值都必须大于等于(或小于等于)其子树中的每个节点的值。
对于每个节点的值都大于等于子树中每个节点的值的堆,叫做“大顶堆”。
对于每个节点的值都小于等于子树中每个节点的值的堆,叫做“小顶堆”。
我们来分析一下下图,看看这几个二叉树是不是堆?是什么堆?
第1个和第2个是大顶堆,第三个是小顶堆,第4个不是堆(堆是完全二叉树,最后一层的子节点都靠左排列,4中最后一层子节点靠右了)。
二、如何实现一个堆
1、如何存储一个堆
完全二叉树比较适合用数组来存储,用数组存储完全二叉树是比较节省存储空间的,因为不需要存储左右子节点的指针,单纯的通过数组的下标,就可以找到一个节点的左右子节点和父节点。
数组存储堆的例子如下:
数组中下标为i的节点的左子节点的下标为 i*2 的节点,右子节点就是下标为 i*2+1 的节点,父节点就是下标为 i/2 的节点。
2、往堆中插入一个元素
向堆中插入一个元素,需要继续满足堆的两个特性。
如下图所示,我们将22放到堆的最后,将会不符合堆的特性,这时候,我们需要进行调整,让其重新满足堆的特性,这个过程叫做“堆化”。
堆话分为两种,从下往上和从上往下,下面讲一下从下往上的堆化方法。
堆化的过程其实并不复杂,就是顺着节点所在的路径,向上或者向下,对比,然后交换。
如下图所示,我们可以让新插入的节点与父节点对比大小。如果不满足子节点小于等于父节点的关于,我们就交换两个节点。一直重复这个过程,直到父子节点之间满足刚说的那种大小关系。
代码如下:
public class Heap {
private int[] a; // 数组,从下标1开始存储数据
private int n; // 堆可以存储的最大数据个数
private int count; // 堆中已经存储的数据个数
public Heap(int capacity) {
a = new int[capacity + 1];
n = capacity;
count = 0;
}
public void insert(int data) {
if (count >= n) return; // 堆满了
++count;
a[count] = data;
int i = count;
while (i/2 > 0 && a[i] > a[i/2]) { // 自下往上堆化
swap(a, i, i/2); // swap()函数作用:交换下标为i和i/2的两个元素
i = i/2;
}
}
}
3、删除堆顶元素
堆中的任何节点的值都大于等于(或者小于等于)子树节点的值,我们可以发现,堆顶元素存储的就是堆中数据的最大值或者最小值。
假设我们构造的是大顶堆,则堆顶元素就是最大的元素,我们删除堆顶元素之后,就需要把第二大的元素放到堆顶,第二大元素肯定会出现在左右子节点中,我们再迭代地删除第二大节点,以此类推,直到叶子节点被删除。
分解图如下图所示,不过这种方法有点问题,就是最后堆化出来的堆并不满足完全二叉树的特性。
实际上,我们可以稍微改变一下思路,如下图所示,我们把最后一个节点放到堆顶,然后利用同样的父子节点对比方法。对于不满足父子节点大小关系的,互换两个节点,并且重复进行这个过程,直到父子节点之间满足大小关系为止。这就是从上往下的堆化方法。
因为这种方法移除的是数组中的最后一个元素,而在堆化的过程中,都是交换操作,不会出现数组中的“空洞”,所以这种方法堆化之后的结果,一定满足完全二叉树的特性。
代码实现如下:
public void removeMax() {
if (count == 0) return -1; // 堆中没有数据
a[1] = a[count];
--count;
heapify(a, count, 1);
}
private void heapify(int[] a, int n, int i) { // 自上往下堆化
while (true) {
int maxPos = i;
if (i*2 <= n && a[i] < a[i*2]) maxPos = i*2;
if (i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1;
if (maxPos == i) break;
swap(a, i, maxPos);
i = maxPos;
}
}