二叉堆是一种特殊的堆,二叉堆是完全二叉树。二叉堆有两种:最大堆和最小堆。最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值[百度百科]。在最大堆中任意子堆都满足父结点的键值总是大于或等于任何一个子节点的键值这一性质。如下图即为一个最大堆。
数组构建完全二叉树
假设当前节点索引为
,有如下三个公式:
- 父节点索引为
- 左孩子索引为
- 右孩子索引为
堆代码实现
基础方法实现
①、使用ArrayList存储堆的元素;②、元素必须是可比较的。
public class MaxHeap<E extends Comparable<E>> {
private List<E> data;
public MaxHeap(int capacity){
data = new ArrayList<>(capacity);
}
public MaxHeap(){
data = new ArrayList<>();
}
/**
* 返回堆中的元素个数
*/
public int size(){
return data.size();
}
/**
* 判断堆是否为空
*/
public boolean isEmpty(){
return data.isEmpty();
}
}
实现公式
在整个最大堆的创建过程中上面的三个公式是十分重要的,很简单实现。
/**
1. 使用数组表示的二叉堆中我们可以使用索引找到父亲节点,左节点和右节点的索引
*/
private int parent(int index){
assert index>=1;
return (index-1)/2;
}
private int leftChild(int index){
return index * 2 + 1;
}
private int rightChild(int index){
return index * 2 + 2;
}
siftUp和siftDown
敲黑板了!这进入重点啦!!!
添加元素
往堆中添加元素步骤:
- 将元素添加到数组的末尾;
- 添加元素后也许不满足堆的性质了,需要对整个堆进行调整(siftUp)。
执行第一步后新节点被添加为40的左孩子,40<95,不符合最大堆的性质。我们需要进行siftUp操作:
- 将当前节点值与其父节点比较:
- 如果节点已经为根节点,siftUp完成
- 如果父节点大于当前节点,siftUp完成;
- 如果父节点小于当前节点,交换两个节点。
- 更新当前节点。重复siftUp。
第一次比较,交换:
第二次比较,交换:
第三次比较,交换:
此时新节点成为了新的根节点,无需再进行调整了。
/**
* 添加元素,sift up
*/
public void add(E e){
data.add(e);//将新增元素加到最末尾
siftUp(data.size()-1);//堆的调整,让其满足最大堆性质
}
private void siftUp(int index){
assert index>=0&&index<data.size();
//当前节点大于其父节点,进入循环继续调整,直至当前节点小于父节点或者已经成为根节点
while(index > 0 && data.get(parent(index)).compareTo(data.get(index))<0){
swap(parent(index),index);
index = parent(index);//更新当前节点的索引
}
}
private void swap(int parent,int child){
assert parent>=0&&parent<data.size()&&child>=0&&child<data.size();
E temp = data.get(parent);
data.set(parent,data.get(child));
data.set(child,temp);
}
移除最大值
最大堆对外只提供最大值的获取方法,移除最大值后我们同样需要对堆进行重新调整(siftDown)。
- 获取根节点(最大值),保存在临时变量temp中;
- 将数组末尾节点与根节点进行交换;
- 进行堆的调整(siftDown).
开始进行siftDown操作:
- 将当前节点(首次为根节点)与左右孩子中较大的节点进行比较;
- 如果当前节点为叶子节点,siftDown完成;
- 如果大于较大值,则已经符合最大堆的性质,siftDown完成;
- 如果小于较大值,则与较大值进行交换
- 更新当前节点,再次进行siftDown操作。
第一次比较,20与80进行交换:
第二次比较,20与40进行交换:
第三次比较,20与30进行交换:
20已经为叶子节点,siftDown操作完成。
/**
* 单纯获取最大值,不删除
*/
public E findMax(){
assert data.size()>0;
return data.get(0);
}
/**
* 从最大堆中取出最大的元素,sift down
*/
public E extractMax(){
E ret = findMax();
swap(0,data.size()-1);//将最末尾的元素放到根节点上
data.remove(data.size()-1);
siftDown(0);
return ret;
}
private void siftDown(int index){
while(leftChild(index)<data.size()){
int j = leftChild(index);//取得左孩子索引
if(j+1<data.size()&&data.get(j+1).compareTo(data.get(j))>0){
//当前节点有右孩子,并且右孩子的值大于左孩子的值
j+=1;//较大值为右孩子,更新j为右孩子索引
}
if(data.get(index).compareTo(data.get(j))>=0){
//当前节点大于较大值,符合最大堆性质,无需再siftDown
break;
}
swap(index,j);//当前节点与较大值交换
index = j;//更新当前节点索引
}
}