二叉堆是基于完全二叉树的;所谓完全二叉树,就是每层的数据按照从左到右的方式存储,即按照层序遍历的顺序存储;每个节点都大于它的左孩子和右孩子,根节点为树中最大的节点的堆为最大堆;相应的,每个节点都小于其左孩子和右孩子,根节点为最小节点的堆为最小堆;但是左右孩子的大小关系是不一定的;
很显然,堆的实现可以是基于二叉树的;但是更简单的方法是基于数组:
根节点的下标为1,对于下标为i的元素,它的左孩子的下标为2*i,右孩子下标为2*i+1,和基于二叉树的实现方式相比较,一个优势是很容易找到节点的父节点,父节点的下标为i/2。这种下标之间的关系是依赖于完全二叉树每层从左到右存储的特点的;
下面是基于数组的一个最大堆的实现,提供了下面一些常见的操作:
详细代码如下:
package com.gby.heap;
import com.itheima.array.Array;
public class MaxHeap<E extends Comparable<E>> {
private Array<E> array;
public MaxHeap() {
array = new Array<E>();
}
public MaxHeap(int capacity){
array = new Array<E>(capacity);
}
public int getSize(){
return array.getSize();
}
public boolean isEmpty(){
return array.isEmpty();
}
public int parent(int index){
if(index==0){
throw new IllegalArgumentException("index--0 doesn't have parent.");
}
return (index-1)/2;
}
public int leftChild(int index){
return 2*(index-1);
}
public int rightChild(int index){
return 2*(index-1) +1;
}
public void add(E e){
//添加元素
array.addLast(e);
//维护堆的特点
siftUp(array.getSize()-1);
}
private void siftUp(int index) {
while(index>0 && array.get(index).compareTo(array.get(parent(index)))>0){
array.swap(index,parent(index));
index = parent(index);
}
}
public E findMax(){
if(array.isEmpty()){
throw new IllegalArgumentException("Array is empty.");
}
return array.getFirst();
}
private void siftDown(int index){
if(index <0 || index>=array.getSize() || array.isEmpty()){
throw new IllegalArgumentException("Index is Illegal or array is empty.");
}
while(leftChild(index)<array.getSize()){ //存在左孩子
int j = leftChild(index);
if(j+1 < array.getSize() && array.get(j+1).compareTo(array.get(j))>0){ //有右孩子并且右孩子大于左孩子
j = j+1;
if(array.get(index).compareTo(array.get(j))>0){
break;
}
array.swap(index, j);
index = j;
}
}
}
public E extractMax(){
if(array.isEmpty()){
throw new IllegalArgumentException("EctractMax failed.There is no element in this maxHeap.");
}
E maxEle = array.getFirst();
array.set(0, array.removeLast());
//通过下沉操作维护最大堆性质
siftDown(0);
return maxEle;
}
public E replace(E newEle){
E max = findMax();
array.set(0, newEle);
siftDown(0);
return max;
}
public void heapify(E[] arr){
array = new Array<E>(arr);
int parent = parent(arr.length-1);
for(int i= parent; i >=0; i--){
siftDown(parent);
}
}
}
下面是实现思路:
1.最大堆中的数据要求父节点大于其左右孩子,所以堆中的元素必须是Comparable的。
2.基于数组实现,内部是一个Array类型的私有成员变量;这里的array是之前数据结构(一)的博客中封装的动态数组,可以自动改变容量,并提供了常用的方法;
3.提供无参和有参的构造方法
4.提供getSize()获取堆的大小,提供isEmpty()判断堆是否为空
根据之前的分析,提供上面三个方法找到一个节点的父节点和左右孩子节点的下标;
向堆中添加元素时,默认先把元素放在数组的最后,即下标为size-1的位置;相关判断在Array类中进行;
在添加一个元素后,很有可能这个元素和它的父节点之间并不满足父节点大于这个节点的条件;如果新添加的元素大于其父亲节点,则我们把它和父节点下标的元素交换位置,在堆中表现为上浮到堆的上一层,成为siftUp();上浮一次,很有可能它还是大于父节点,继续上浮,直到它小于父节点或者到达完全二叉树的根,即数组下标为0的位置。
堆的设计是服务于优先队列,出队的为优先级最大的或者最小的,对应堆中取出的元素应当是堆顶的元素,对应最大堆的extractMax()方法。
如果直接移除下标为0的元素,则其左右子树需要整体移动,并产生新的根,操作复杂。一个简单的方法是用数组的最后一个元素替代下标为0的元素。很显然,这样做很容易会破坏根最大的性质; 那么,如果根有新的根大于左右孩子,直接结束。否则和左右孩子中的最大值交换位置(如果有左右孩子);
replace方法是将堆顶的元素替换为用户传递进去的元素,在替换后通过siftDown()维护堆的性质;
heapify方法是用户传递一个数组,把它变成一个堆;
一种方法是,循环遍历arr,将每个元素通过add方法添加到一个堆中;
另一种方法是,把该数组看做一个完全二叉树,从第一个非叶子节点到根节点进行遍历,对每个遍历到的节点进行下沉操作,整理成一个堆,上面的代码就是第二种方法;
以上就是堆这种数据结构的常见操作和实现。