优先队列
很多时候我们可能需要处理有序的元素,但是并不需要全部有序,可能只需要取出某个优先级较高的元素(最大优先队列),只处理当前键值较大元素,API为插入元素和删除最大元素。这种数据类型为优先队列。
API:
public class MaxPQ<Key extends Comparable<Key>> {
// 创建一个优先队列
public MaxPQ(){}
// 创建一个初始容量为max的优先队列
public MaxPQ(int max){}
// 用数组a的元素创建一个优先队列
public MaxPQ(Key[] a){}
// 向优先队列插入一个元素
public void insert(Key v){}
// 返回最大元素
public Key max(){return null;}
// 删除并返回最大元素
public Key delMax(){return null;}
// 队列是否为空
public boolean isEmpty(){return false;}
// 优先队列元素个数
public int size(){return 0;}
}
或许我们可以将数组排序后取出最值即可,那么增长数量级是比较高的,并且我们并不需要整个数组是有序的,使用堆结构进行处理是个不错的选择。
二叉堆
一组堆有序的完全二叉树排序的元素(不使用第一个位置),堆有序指父节点大于等于两个子节点。
实现策略
插入元素后将新元素与父节点做比较如果大于父节点则与父节点交换,然后在与父节点的父节点比较,直到小于父节点或者为根节点,整个过程称为上浮。代码如下:
// 元素上浮
private void swim(int k){
while (k>1&&less(k/2,k)){
exch(k/2,k);// 父节点小于子节点交换
k/=2;// 一直上浮
}
}
示意图
基于二叉堆的完全二叉树根节点即为最大元素,删除元素只需将第一个元素取出即可(根节点),然后将第一个节点与最后一个结点交换,接着将第一个结点与两个子结点作比较,当小于较大的子节点时交换,直到大于两个子结点或者到达底部时结束。整个过程称为下沉操作,代码如下:
// 元素下沉
private void sink(int k){
while (2*k<N){
int j = 2*k;
if(j<N&&less(j,j+1))j++;// j小于j+1则较大的是j+1
if(!less(k,j)) break;// 当前值大于最大的子节点不进行交换
exch(j,k);
k=j;
}
}
示意图
插入和删除实现如下:
// 向优先队列插入一个元素
public void insert(Key v){
pq[++N] = v;
swim(N);// 新插入的结点上浮找到自己位置
}
// 返回最大元素
public Key max(){return pq[1];}
// 删除并返回最大元素
public Key delMax(){
Key max = pq[1];
exch(1,N--);// 将最后一个与第一个交换后下沉即可
pq[N+1] =null;// 放置对象游离
sink(1);
return max;
}
分析
由树状图可知,插入和删除操作的运行时间取决于树的高度,在归并排序中有分析可知运行时间为对数级别的。插入操作最多不超过lgN+1次比较,删除操作不超过2lgN次比较(下沉中有两次比较)。如果使用数组进行排序后取值,意味着我们需要线性级别的运行时间。而基于堆有序的完全二叉树实现的优先队列突破了这个限制。
改进
- 多叉堆
可以构建二叉堆同样可以构建多叉堆,假设为三叉堆,则位置k的结点大于3k-1、3k、3k+1结点的元素,构建树高度为3n=N,增长数量级为log3N。根据归纳可知d叉堆的增长数量级为logdN - 动态调整数组大小
插入元素调整数组加倍,删除时减小数组为一半,无需关注队列大小的限制
堆排序
public class Heap {
private Heap(){}
private static boolean less(Comparable[] a,int vIndex,int wIndex){
return a[vIndex-1].compareTo(a[wIndex-1])<0;
}
private static void exch(Comparable[] a,int vIndex,int wIndex){
Comparable temp = a[vIndex-1];
a[vIndex-1] = a[wIndex-1];
a[wIndex-1] = temp;
}
// 下沉操作
private static void sink(Comparable[] a,int k,int N){
while (2*k<=N){
int j=2*k;
if(j<N&&less(a,j,j+1))j++;
if(!less(a,k,j))break;
exch(a,k,j);
k=j;
}
}
public static void sort(Comparable[] a){
int N =a.length;
// 构建二叉堆,倒序下沉的话可以跳过单节点的情况
for(int i=N/2;i>=1;i--){
sink(a,i,N);
}
// 从小到大排序
while (N>1){
exch(a,1,N--);// 将最大值放在最后
sink(a,1,N);// 交换过来的值找到自己的位置
}
}
}
由于为了与二叉堆一致的从1开始计数,所以less与exch对索引的操作都进行了-1
实现策略
基于二叉堆的排序,先将数组按照下沉的方法一个一个构成最大优先队列然后在将第一个(最大值)与最后一个交换后,将当前第一个值进行下沉找到自己位置,此时又是堆有序的完全二叉树,继续与倒数第二个交换,最终为从小到大的排序。
分析
堆排序是基于二叉堆的排序,是一种非常优雅的排序方法。先对数组进行二叉堆的构造,然后将最大值依次放在最后。在进行二叉堆构造时可以使用一个一个上浮的方法,但是在使用的时候我们使用下沉的方法,下沉我们只需对1/2的元素进行下沉操作,最后的单节点元素我们不需要进行下沉操作。用下沉操作进行二叉堆的构造只需最多2N次比较和N次交换。证明如下:
假设高度为h,那么有根节点交换次数最多为h次,第二层交换次数最多为(h-1)次,总的交换次数为:
h+(h-1)*2+(h-2)*2^2^+....+2^h^(h-h)=2^h+1^-h-2
,可以使用归纳法证明,证明如下:
所以整体的堆排序需要最多2N+2NlgN
次比较,2N是构造二叉堆时的比较次数,2NlgN
由前文二叉堆构建可知一次下沉操作最多需要2lgN次比较,N个下沉则需要2NlgN
次比较。以及一半的交换。
优势
能够同时最优的利用好时间和空间,最差情况下也能保证~2NlgN