优先队列是一种特殊的队列,只是弹出元素时弹出的是(权重或者某个指定key)最大的元素。基于有序数组、无序数组、有序链表的实现类似于栈和队列的算法,这里说说基于完全二叉树的最大堆实现。
完全二叉树的层次遍历序列和数组的索引顺序完全一致,这里用数组来存储二叉树的节点值,数组索引之间的关系可以表示树节点的父子关系:某节点在数组中索引为k,其父节点索引为k/2,左右子节点为2k和2k+1(k>=1)。最大堆要求:所有父节点的值大于等于子节点。的数组大小和堆的规模两者需要协调,在插入和弹出元素时,我们需要维护这个数组和堆的大小,数据结构如下:
public class HeapMaxPQ<Key extends Comparable<Key>> {
public static final int DEFAULT_SIZE=16;
public Key[] pq;
public int size;
}
在向堆里面插入元素,先将元素放在堆数组元素序列尾部(二叉树的最底部最右边的空位),再进行上浮操作。弹出(最大)元素时,堆顶就是最大元素,将堆顶元素和堆尾元素置换,此时堆顶元素进行下沉操作,堆尾元素清空弹出,堆规模减一。上浮和下称操作在插入和弹出操作中最为基础,而且让插入和弹出操作变得简单容易实现。
代码如下:
private void swim(int i)
{
while(i/2>=1)
{
if(!less(i,i/2))
exchange(i,i/2);
else
break;
i/=2;
}
}
private void sink(int i)
{
while(2*i<=this.size)
{
int j=2*i;
if(j<this.size && less(j,j+1)) j++;//索引为i的值与其子节点中较大的进行比较
if(!less(j,i))
exchange(i,j);
else
break;
i*=2;
}
}
有了上浮和下称操作,插入和弹出操作就很简单了:
public void insert(Key k) {
if(this.pq.length-1==this.size)
this.resize(this.pq.length*2);//调整数组大小以适应堆的大小
this.pq[++this.size]=k;
swim(this.size);
}
public Key delMax() {
Key max=this.pq[1];
this.pq[1]=this.pq[this.size];
this.pq[this.size]=null;
this.size--;
sink(1);
return max;
}
上面的几个操作中涉及到数组大小的调整和比较交换等基本操作,代码如下:
public HeapMaxPQ(){
this(DEFAULT_SIZE);
}
@SuppressWarnings("unchecked")
public HeapMaxPQ(int max){
this.pq=(Key[]) new Comparable[max+1];//有效索引为1-max
this.size=0;
}
public HeapMaxPQ(Key[] keys){
for(int i=0;i<keys.length;i++)
this.insert(keys[i]);
}
@SuppressWarnings("unchecked")
private void resize(int new_capacity)
{
if(new_capacity>this.pq.length)
{
Key[] tmp=(Key[]) new Comparable[new_capacity];
for(int i=1;i<this.pq.length;i++)
tmp[i]=this.pq[i];
this.pq=tmp;
}
}
private boolean less(int a,int b)
{
if(this.pq[a].compareTo(this.pq[b])<=0)
return true;
else
return false;
}
private void exchange(int i,int j)
{
Key tmp=this.pq[i];
this.pq[i]=this.pq[j];
this.pq[j]=tmp;
}
上述算法思想借鉴学习了Princeton第四版《Algorithms》。