任意一个子节点总是大于等于或者小于等于父节点的完全二叉树称之为堆,根据字节点和父节点的大小关系,堆又分为大顶堆和小顶堆
堆本质是一棵完全二叉树,则完全二叉树的所有性质都适合于堆。
对于二叉树我们一般定义节点的数据结构如下:
public class Node<T> { public T data; public Node left; //左孩子节点 public Node right; //右孩子节点 public Node parent; //父节点}
但是对于完全二叉树,将其按照层序遍历得到包含二叉树中所有元素的序列(\({k_1,k_2,...,k_i,...k_n}\)),总是有给定一个节点\(k_i\),那么其左孩子节点为\(k_{2i+1}\),右孩子为\(k_{2i+2}\),其父节点为\(k_{(i-1)/2}\),那么就可以完全省略掉树节点中左孩子、右孩子以及父节点的相关定义,使用一维数组来存储堆。图1所示的堆使用数组表示后如图2所示
以图1表示的大顶堆为例,将其写成数组形式为{70,58,5,34,20,4,3,13,4},如果此时将数组尾部的4换为60,那么此时如何将其调整一个最大堆呢?
如果堆的有序性因为某个节点的变化变得比其父节点更大(小),那么可以通过交换它与它的父节点来修复堆,如果交换后这个节点比它的两个子节点都大(小)(其中一个节点是它之前的兄弟节点,一个节点是交换之前的父节点),那么它有可能比它新的父节点还大(小),那么可以使用相同的办法一遍一遍交换使其恢复堆得秩序,直到碰到一个比其更大(小)的父节点或者至堆顶(图4),这个操作称之上浮
代码实现:
private void siftUp(int k) { while (k > 0) { int parent = (k - 1) >>> 1; if (!less(parent, k)) break; swap(parent, k); k = parent; }}
如果堆的有序性因为某个节点的变化变得比其两个子节点(或其中一个)更小(大),那么可以通过交互它与它子节点中较大的一个来修复堆,交换后有可能在子节点处继续打破堆得有序性,那么便需要继续这一过程使堆恢复有序性(图5)。这个操作称之为下沉操作。
图5显示了根节点由70变为30后的整理堆的一个过程,使用的就是下沉操作
private void siftDown(int k) { int mid = queue.length >>> 1; while (k < mid) { int child = (k << 1) + 1; int r = child + 1; if (r < queue.length && less(child, r)) child = r; if (!less(k, child)) break; swap(k, child); k = child; }}
如何将给定的的\(N\)个元素构造成一个堆呢?
siftUp
的方法将其放到合适的位置siftDown
的方式使的新的子树满足堆得有序性protected void buildHeap() { int i = (queue.size() >>> 1) - 1; while (i >= 0) { siftDown(i); i--; }}
先说一个结论:
下文将按照从大到小排序来分析堆排序
那么我们可以尝试将一个小顶堆的堆顶和堆最后一个元素(待排序列的最后一个元素)交换,再将除最后一个元素之外的堆通过下沉或者上浮的方式重新构造成一个小顶堆,再将堆顶元素与倒数第二个元素交换,依次递归,得到一个有序的序列。
代码示例:
public void sort() { int N = this.size() - 1; for (int i = N ; i >= 0; i--) { swap(0, i); siftDown(0, i); }}private void siftDown(int k, int N) { int mid = N >>> 1; while (k < mid) { int child = (k << 1) + 1; int r = child + 1; if (r < N && less(child, r)) child = r; if (!less(k, child)) break; swap(k, child); k = child; }}
[1] 算法第4版