堆是一种完全二叉树(每个结点最多有两个子节点,且结点总是有限从最左边排列)。下图是一个最大堆(父节点总是比子节点大)。
需要注意的是左右两个子节点之间并没有确定的大小关系。
最大堆
由于堆的定义,堆具有如下性质:
- 位置为k的节点孩子节点位置为2k, 2k+1;
- 位置为k的节点的父节点位置为[2/k]向下取整。
所以堆可以使用数组来存储。为了计算方便,一般把数组的第一个位置下标0空出来,从下标1开始放堆的元素。
堆的接口以下是常见的堆接口, 代码地址
/**
* 堆接口
*/
public interface IHeap<T> {
int size(); // 当前元素的长度
boolean isEmpty(); // 判空
void add(T data); // 添加
T pop(); // 弹出堆顶元素
T remove(int i); // 移除某位置元素
T peek(); // 过去堆顶元素
}
堆的普通实现
成员变量
为了灵活控制对的排序方式,这里使用了泛型和Comparator接口。
public class MyHeap<T> implements IHeap<T> {
T[] datas; // 存放数据的数组 datas[1..n]
private int count = 0; // 计数
private Comparator<T> comparator; // 比较器
// ...
}
私有的辅助方法
为了代码易读性,增加了两个私有方法:
/**
* 比较两个数,看谁上浮
* @param i 第一个数
* @param j 第二个数
* @return true 为i上浮, false 为j上浮
*/
private boolean needSwim(int i, int j) {
return comparator.compare(datas[i], datas[j]) < 0;
}
/**
* 交换两个数
* @param i 第一个数的位置
* @param j 第二个数的位置
*/
private void exchange(int i, int j) {
T temp = datas[i];
datas[i] = datas[j];
datas[j] = temp;
}
上浮与下沉
堆的主要操作之一,通过与父节点或子节点的不断比较与交换,最终维护堆的性质。其中下沉要稍微复杂一点,因为要判断与左孩子交换还是右孩子交换,同时需要做边界判定。
/**
* 上浮
* @param k 开始上浮的位置
*/
private void swim(int k) {
while (k > 1 && needSwim(k, k / 2)) {
exchange(k, k / 2);
k /= 2;
}
}
/**
* 下沉
* @param k 开始下沉的位置
*/
private void sink(int k) {
while (2 * k <= count) {
int j = 2 * k;
// 判断是否超出范围,再判断j 和 j + 1 那个更需要上浮
if (j < count && needSwim(j + 1, j))
j++; // 跟需要上浮的那个换
if (needSwim(j, k))
exchange(k, j);
else break;
k = j;
}
}
构造函数
构造函数主要有两个,分别是:
- 建空堆
-
使用数组建堆
// 使用空数组构造,容量为capacity public MyHeap(int capacity, Comparator<T> comparator) { this.datas = (T[]) new Object[capacity + 1]; this.comparator = comparator; } // 使用已给的数组构造,容量为数组长度 public MyHeap(T[] datas, Comparator<T> comparator) { this.count = datas.length; this.datas = (T[]) new Object[count + 1]; this.comparator = comparator; System.arraycopy(datas, 0, this.datas, 1, count); // 对前半部分逆序下沉,构造堆 for (int k = count / 2; k >= 1; k--) sink(k); }