堆定义
生活中需要使用优先队列, 比如cpu调度算法,线程调度算法都需要把优先级高的任务装入一个优先队列PriorityQueue。这个需求是很频繁的。优先级队列其实就是最大最小堆,本文的堆都是二叉堆。
堆定义: 当一棵完全二叉树的每一个节点都大于(小于)等于它的两个子节点,那么它就是最大(小)堆。
最大堆堆算法
我们以最大堆为例子,用N+1的数组pq[N+1]表示容量为N的堆。pq[0]作为哨兵不使用,填入数组元素数量或者其他范型值。元素存在pq[1]~pq[N]中。最大堆这种数据结构最主要的方法是:
- 创建堆
- 插入堆元素
- 删除堆元素
最大堆需要实时维护最大堆性质,比如下图:
index=6的地方需要插入58, 但是插入后58比父节点31大也比爷爷节点44大,所以需要循环一次次维护新节点插入以后堆的性质。
最大堆插入/删除元素操作需要自下而上的堆有序化(上浮)和自上而下的堆有序化(下沉)函数来帮助完成:
// 上浮
private void swim(int k) {
while(k > 1 && less(k/2, k)) { //index=k/2的元素小于index=k的元素
swap(k/2, k); //交换index=k/2和index=k的元素
k = k/2;
}
}
// 下沉
private void sink(int k) {
while(2*k <= N) {
int j = 2*k;
if(j < N && less(j, j+1)) //取得k节点的两个子节点中大一点的那个节点的下标
j++;
swap(k, j); //交换下沉节点和那个子节点的元素
k = j;
}
}
最大堆堆代码
public class MaxHeap<T extends Comparable<T>> {
private T[] pq;
private int N = 0; //元素存在于PQ[1]~PQ[N]中,pq[0]存放堆中元素数量
public MaxHeap(int capacity) {
pq = (T[]) new Comparable[capacity + 1];
}
public boolean isEmpty() {
return N==0;
}
public int size() {
return N;
}
public void insert(T e) {
pq[++N] = e;
swim(N);
}
public T deleteMax() {
T max = pq[1]; //下标1的节点是最大值
swap(1, N); //将第一个元素和最后一个元素交换
pq[N] = null; //GC
sink(1); //恢复删除以后堆的有序
--N;
return max;
}
private void swim(int k) {
while(k > 1 && less(k/2, k)) { //index=k/2的元素小于index=k的元素
swap(k/2, k); //交换index=k/2和index=k的元素
k = k/2;
}
}
private void sink(int k) {
while(2*k <= N) {
int j = 2*k;
if(j < N && less(j, j+1)) //取得k节点的两个子节点中大一点的那个节点的下标
j++;
swap(k, j); //交换下沉节点和那个子节点的元素
k = j;
}
}
// swap函数和less函数自己去实现,这里不展开。
}
最大(小)堆的实际应用: 堆排序
由于每次出队的都是在剩下元素里面最大(小)的, 所以只要把数组的元素放到一个pq里, 然后依次poll出来, 得到的序列就是排序好了的。
不管是插入还是删除操作, 每次调整的复杂度为log(h) (堆的高度), 所以算法复杂度为 O(NlgN). 实际使用的时候效率比快速排序/合并排序略差。
heapsort里第一步是要建立一个最大堆,最直白的建堆操作就是和上文一样新建一个空的数组然后不断向里面加入元素,同时维护堆(空间复杂度N, 时间复杂度NlgN),但其实这个操作可以做的更好:我们先直接把数组a当作最大堆pq[]数组 , 现在显然它不满足最大堆性质, 只需要 多次使用sink()进行调整即可。
假设堆数组维护的完全二叉树一共有h(=lgN)层, 由于最后一层的节点不必调用sink(), 我们只要从倒数第二层开始调用sink()即可, 结合前面提到的最大堆pq的性质(N/2以后的节点都在最后一层), 写法很简单(简单起见认为pq数组也是把第0个元素空出来好了):
public class HeapSort {
public static void sort(Comparable[] pq) {
int N = pq.length - 1; //index=0的元素不使用,N是最后一个的index
buildHeap(pq, N);
while (N > 1) {
swap(pq, 1, N);
sink(pq, 1, --N);
}
}
// 从原始数组里面建立最大堆
// 这个操作的时间复杂度是O(N)的 ! 为什么呢?
// 第k层节点有2^k个节点, 这一层的节点向下调整最多会进行h-k步, 所以计算量是一个求和表达式:
// Sigma( 2^k * (h-k) ) for k=0,...,h-1 = h + 2(h-1) + 4(h-2) + 2^h(0) = 2^h+1 - h - 2 = N - (h - 1) <= N
// 复杂度为O(N)
private static buildHeap(Comparable[] pq, int N) {
for (int i = N / 2; i >= 1; i--) {
sink(pq, i, N);
}
}
// logN复杂度
private static void sink(Comparable[] pq, int k, int N) {
while (2 * k <= N) {
int j = 2 * k;
if (j < N && less(pq, j, j + 1)) //取得k节点的两个子节点中大一点的那个节点的下标
j++;
if (!less(pq, k, j))
break;
swap(pq, k, j); //交换下沉节点和那个子节点的元素
k = j;
}
}
private static boolean less(Comparable[] pq, int i, int j) {
if (pq[i].compareTo(pq[j]) < 0)
return true;
else
return false;
}
private static void swap(Comparable[] pq, int i, int j) {
Comparable t = pq[i];
pq[i] = pq[j];
pq[j] = t;
}
}
Heap construction uses ≤ 2 N compares and exchanges.
Heapsort uses ≤ 2 N lg N compares and exchanges.
Not Stable algorithm
堆排序算法过程(demo过程)
第一步: 最大堆Heap的建立。
private static void buildHeap().
第二步: 循环sortdown步骤:
- 交换下标1的最大元素和下标为N的最后一个元素。
- 使下标最后的元素离开数组,再不断进行sink(a, 1, N)操作。