说起堆,有过编程经验的小伙伴可能会想起来是什么,那么二叉堆是什么呢?有什么用呢?我们自己动手如何实现一个呢?带着这些疑问,开始~
是什么?
- 是一个特殊的堆,是一棵完全二叉树或者是近似完全二叉树(二叉树,上篇文章详细介绍过)
- 每个节点的左子树和右子树都是一个二叉堆
- 当父节点的键值总是大于或等于任何一个子节点的键值时为
最大堆。当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆 - 二叉堆一般用数组来表示。如果根节点在数组中的位置是1,第n个位置的子节点分别在2n和 2n+1。因此,第1个位置的子节点在2和3,第2个位置的子节点在4和5。以此类推。这种基于1的数组存储方式便于寻找父节点和子节点
- 下图所示是一个二叉堆(最大堆),用数组表示:11 9 10 5 6 7 8 1 2 3 4
实操
以最大堆为例子,探索一下对其的基本操作
添加元素
- 以上图中的例子,假如添加一个元素15。
- 我们知道其底层使用数组来存储,那么添加一个元素,也即在数组的最后添加一个元素,也即在节点7的左子树添加15元素,结果如下图所示:
- 根据最大堆的性质,父节点要大于其左右子树,7不大于新添加的15,所以这里存在一个操作,也即与父节点进行比较,如果新添加的元素大于其父节点,那么就和父节点交换位置。交换之后继续与其父节点比较,直到满足为止
- 在这个例子中,7小于15 所以7和15 要转换位置:
- 转换之后15与10比较,10小于15,所以需要转换位置
- 转换之后,15和11比较,11小于15,再次转换位置
- 到此,15变成整个树的根节点,满足二叉堆的性质,添加结束,结果如图:
取出最大的元素(删除最大元素)
将元素15取出,也即将整棵树的根节点删除,其左右子树如何操作呢?
- 这里遵循的方法原则是将存储的数组中最后一个元素,也即二叉堆中元素7顶替元素15的位置,然后依次比较以满足最大堆性质,移动之后结果:
- 元素7分别于左右子树中最大的比较也即与11比较,发现7小于11,那么元素7与元素11调换位置
- 调换之后继续与左右子树中最大值比较,也即与元素10比较,发现7小于10,那么元素7与元素10调换位置
- 到此调整完成,满足最大堆性质:
代码实操
以java代码为例实现最大堆
//因为存在比较的过程,所以实现Comparable接口
//底层使用数组来存储元素
public class MaxHeap<E extends Comparable<E>> {
private Array<E> data;
public MaxHeap(int capacity){
data = new Array<>(capacity);
}
public MaxHeap(){
data = new Array<>();
}
// 返回堆中的元素个数
public int size(){
return data.getSize();
}
// 返回一个布尔值, 表示堆中是否为空
public boolean isEmpty(){
return data.isEmpty();
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
private int parent(int index){
if(index == 0){
throw new IllegalArgumentException("没有父节点");
}
return (index - 1) / 2;
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
private int leftChild(int index){
return index * 2 + 1;
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
private int rightChild(int index){
return index * 2 + 2;
}
// 向堆中添加元素
public void add(E e){
data.addLast(e);
siftUp(data.getSize() - 1);
}
//元素上移操作,上面理论部分详细解释过这个过程,小伙伴们可以结合这个递归函数体会一下
private void siftUp(int k){
while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0 ){
data.swap(k, parent(k));
k = parent(k);
}
}
// 看堆中的最大元素
public E findMax(){
if(data.getSize() == 0){
throw new IllegalArgumentException("无数据");
}
return data.get(0);
}
// 取出堆中最大元素
//最大的元素也即整个树的根节点,也就是数组的头元素
//也即删除最大的元素,上面理论部分有详细的步骤说明删除之后,元素如何调整
//结合代码再次体会
public E extractMax(){
E ret = findMax();
data.swap(0, data.getSize() - 1);
data.removeLast();
siftDown(0);
return ret;
}
//元素下移操作
private void siftDown(int k){
while(leftChild(k) < data.getSize()){
int j = leftChild(k); // 在此轮循环中,data[k]和data[j]交换位置
if( j + 1 < data.getSize() &&
data.get(j + 1).compareTo(data.get(j)) > 0 ){
j ++;
}
// data[j] 是 leftChild 和 rightChild 中的最大值
if(data.get(k).compareTo(data.get(j)) >= 0 ){
break;
}
data.swap(k, j);
k = j;
}
}
}
- 结合代码与理论可以体会其中实现的细节,也能再次体会递归这个方法的妙用
- 感兴趣小伙伴可以自己尝试使用任意一个数组构造出一个二叉堆
到此,本篇文章就讲解完了,其中介绍了二叉堆是什么,并以最大堆为例子,介绍了添加元素,删除最大元素的实现思路和代码实现,认真看完的小伙伴肯定有所收获