二叉堆接口
public interface Heap<E> {
int size();
boolean isEmpty();
void clear();
void add(E element);//添加元素到堆
E get();//取堆顶元素
E remove();//删除堆顶元素
E replace(E element);//删除堆顶元素的同时插入一个新元素
}
大顶二叉堆的实现
存储结构
对于采用二叉树结构的堆,使用数组存储结构,可以不需要有额外的保存左右孩子节点和父亲节点的指针域,节省空间,而且用数组存储实现简单。下标为n的节点的左右孩子节点(如果有的话)下标为2n+1和2n+2,下标为n的节点的父节点(如果有的话)下标为n/2-1。
大顶二叉堆性质
1.每个节点的值域按照比较方法来比较,比较结果将大于它的左右节点。而对于不是父子关系的节点,对谁大谁小没有要求。
2.大顶二叉堆的数组对应的二叉树是一棵完全二叉树,该完全二叉树中非叶子节点的下标值大于size/2
一些必要的辅助方法
protected int compare(E e1,E e2) {
return comparator!=null?comparator.compare(e1, e2):((Comparable)e1).compareTo(e2);
}
//数组扩容检查,如果需要扩容就扩容为原来的1.5倍
private void ensureCapacity(int capacity) {
int oldCapacity=elements.length;
if(oldCapacity>=capacity) return;//不需要扩容
int newCapacity=oldCapacity+ (oldCapacity>>1);
E[] newElements=(E[])new Object[newCapacity];
for(int i=0;i<size;i++) {
newElements[i]=elements[i];
}
elements=newElements;
}
//元素非空检查
private void elementNotNullCheck(E element) {
if(element==null) {
throw new IllegalArgumentException("element must not be null.");
}
}
//二叉堆非空检查
private void emptyCheck() {
if(size==0) {
throw new IndexOutOfBoundsException("Heap is empty.");
}
}
元素添加方法
在对元素进行非空检查,并对存储空间进行溢出检查后,即可将元素先添加到二叉堆的数组末尾,然后采用 “上滤” 的方式恢复二叉堆。上滤的具体做法是,将当前节点node(此处为数组尾元素)与其父亲节点parent进行大小比较,如果当前节点node大于父亲节点parent,就交换它们的位置,然后再以交换后的parent的下标为node的下标,继续进行比较。直到遇到某次比较其父节点不小于当前节点,或者直到当前节点没有父节点时结束。
对于该上滤处理逻辑,可进行如下优化:由于当前节点node在每次与父节点比较过后其位置并未最终确定下来,所以可以在循环中先不将node放置在其父节点的位置,而是在找到合适位置后,退出循环,将node放置在该合适位置。这样减少了赋值次数。
@Override
public void add(E element) {
elementNotNullCheck(element);
ensureCapacity(size+1);
elements[size++]=element;//先将元素添加到数组末尾
ShifUp(size-1);//再恢复二叉堆性质
}
private void ShifUp(int index) {
E node=elements[index];
while(index>0) {
//计算并取出其父节点
int parentIndex=(index-1)>>1;
E parent = elements[parentIndex];
//如果父节点不小于当前node, 退出循环执行将node放到index位置的语句
if(compare(node, parent)<=0) break;
//当父节点小于node时,将父节点放到原node节点位置,并更改index,然后进行循环。
elements[index]=parent;
index=parentIndex;
}
//退出循环时已经找到了合适的下标位置
elements[index]=node;
}
元素移除方法
元素移除过程应先判断二叉堆是否为空,然后在进行移除操作。具体做法是:保存下标为0(即二叉树的root节点)的元素作为返回值,让数组最后一个元素(下标为size-1)覆盖下标为0的元素,并将数组最后一个元素置空,同时size–。此时元素已被删除,但二叉堆很可能不再符合大顶二叉堆性质,此时对下标为0的元素进行 “下滤” 操作,具体做法是:比较该节点的左右孩子节点,如果当前节点小于左右孩子节点中较大值,就将当前节点与其较大孩子节点交换位置,然后对交换位置后的节点进行同样的操作,即循环。循环结束条件是当节点不存在左右孩子节点时结束,亦即当下标大于size/2时(见大顶二叉堆性质2)。
@Override
public E remove() {
emptyCheck();//先进行空检查,非空才能进行下面的删除操作
int lastSize=--size;
E root = elements[0];
elements[0]=elements[lastSize];
elements[lastSize]=null;
ShifDown(0);
return root;
}
//下滤过程
private void ShifDown(int index) {
int half=size>>1;//完全二叉树中,非叶子节点的数量为floor(n/2)
E node =elements[index];
//index小于第一个叶子节点的下标
while(index < half) {//保证当index有子节点时,才能进入该循环
int ChildIndex=(index<<1)+1;//默认取左子节点,
E Child =elements[ChildIndex];
if((ChildIndex+1) < size) {//如果有右子节点
int right=ChildIndex+1;//计算出右孩子节点下标
if(compare(elements[right], Child)>0) {
//如果右孩子节点较大,更换Child为右孩子节点
Child=elements[right];
ChildIndex=right;
}
}
//此时找到个子节点中的较大值
if(compare(Child, node)<=0) {//当较大子节点小于或等于node时,
break;
}
//将较大孩子上移,同时index更新
elements[index]=Child;
index=ChildIndex;
}
//最后退出循环,将node,放到合适位置
elements[index]=node;
}
replace方法
在以上各个方法的基础上, replace方法如下:
@Override
public E replace(E element) {
elementNotNullCheck(element);
if(size==0) {
elements[size++]=element;
return null;
}
//先让element覆盖下标为0的位置,再对该位置进行下滤以恢复性质
E root = elements[0];
elements[0]=element;
ShifDown(0);
return root;
}
批量添加方法
对于二叉堆,应该有一个可以批量添加形成二叉堆的方法,传入一个数组,直接以该数组中元素构造二叉堆。
对于批量添加,增加两个构造函数,用于允许传入一个E类型数组。先将传入的数组中所有元素复制到elements中,再调用heapify方法对elements中元素进行调整,使其恢复性质。
heapify对每一个非叶子元素进行从小到上的下滤,该过程所有能够时结果变为符合二叉堆性质的二叉树的原因是:每对一个元素进行下滤之前,其左右子树都已经调整为一棵符合二叉堆性质的二叉树。
heapify也可以采用从上到下的下滤,该过程类似于将数组中元素依次添加到二叉堆中,性能较差。
public BinaryHeap(E[] elements,Comparator<E> comparator) {
super(comparator);
if(elements==null||elements.length==0) {
this.elements=(E[])new Object[DEFAULT_CAPACITY];//创建默认容量的数组
}else {
size=elements.length;
int capacity=Math.max(DEFAULT_CAPACITY, elements.length);
this.elements=(E[])new Object[capacity];
//采用深拷贝,而不用this.elements=elements;的方式,是为了避免用户在外部修改heap
for(int i=0;i<elements.length;i++) {
this.elements[i]=elements[i];
}
heapify();
}
}
public BinaryHeap(E[] elements) {
super();
if(elements==null||elements.length==0) {
this.elements=(E[])new Object[DEFAULT_CAPACITY];//创建默认容量的数组
}else {
size=elements.length;
int capacity=Math.max(DEFAULT_CAPACITY, elements.length);
this.elements=(E[])new Object[capacity];
//采用深拷贝,而不用this.elements=elements;的方式,是为了避免用户在外部修改heap
for(int i=0;i<elements.length;i++) {
this.elements[i]=elements[i];
}
heapify();
}
}
//批量添加 自下而上的下滤 效率较高 (n)
private void heapify() {
//从最后一个非叶子节点的位置开始
for(int i=(size>>1)-1;i>=0;i--) {//一定得是i>=0,不能是i>0
ShifDown(i);
}
}
//批量添加 自上而下的上滤 (nlogn)
private void heapify_1() {
for(int i=1;i<size;i++) {
ShifUp(i);
}
}
小顶二叉堆的实现
对于小顶二叉堆,在实现大顶二叉堆的基础上,直接继承大顶二叉堆,重新比较逻辑(在比较方法中更改e1和e2的位置,即元素较小的,认为它较大)。小顶二叉堆的实现所有代码如下:
public class minBinaryHeap<E> extends BinaryHeap<E> {
public minBinaryHeap() {
super();
}
public minBinaryHeap(E[] elements,Comparator<E> comparator) {
super(elements,comparator);
}
public minBinaryHeap(E[] elements) {
super(elements);
}
public minBinaryHeap(Comparator<E> comparator) {
super(comparator);
}
//只要重写比较方法,就可以实现小顶二叉堆
@Override
protected int compare(E e1, E e2) {
return comparator!=null?comparator.compare(e2, e1):((Comparable)e2).compareTo(e1);
}
}
应用:topK问题
问题简单描述:对于大量无序数据,找出最大的前k个数。
处理分析和代码
使用小顶堆,遍历无序数据数组,将前k个元素用add方法添加到小顶二叉堆中,然后对于之后的元素所有元素,如果值大于当前小顶堆堆顶元素,使用replace方法替换堆顶元素。
代码如下:
private static void text4() {
Integer[] data = {68,72,43,59,38,10,90,19,23,45,42,70,91,85};
minBinaryHeap<Integer> heap=new minBinaryHeap<>();
int k=4;
for(int i=0;i<data.length;i++) {
if(i<k) {
heap.add(data[i]);
}else {
if(data[i]>heap.get()) {
heap.replace(data[i]);
}
}
}
System.out.println(heap.toString());
}
public static void main(String[] args) {
text4();
}
运行结果
使用重写的toString方法打印输出: