在实际应用中,我们常常不一定要求整个数组全部有序,或者不需要一次就将它们排序,可能只需要当前数组的键值最大的元素或最小的元素,这时就类似于总在处理下一个优先级最高的元素,在这种情况下一个合适的数据结构应该支持两种操作:删除最大元素和插入元素。这种类型叫做优先队列
实现这种数据结构的方法有两种,一种是简单的数组或链表来实现,一种是具有高效的二叉堆来实现,在本文中我们就是采用二叉堆来实现的。
本文的目的是写一个实现二叉堆的API,以便后续需要优先队列的这种数据结构时使用。优先队列最重要的操作就是删除最大元素(或最小,其实差别就在于在less()函数比较方向改变一下就行)和插入元素。为了保证灵活性,我们采用泛型,将实现了Comparable接口的数据类型作为参数Key。
表:泛型 优先队列的API
------------------------------------------------------------------------------------------------------------
public class MaxPQ<Key extends Comparable<Key>>
------------------------------------------------------------------------------------------------------------
MaxPQ 创建一个优先队列
MaxPQ(int max) 创建一个初始容量为max的优先队列
MaxPQ(Key[] a) 用a[]中的元素创建一个优先队列
---------------------------------------------------------------------------------------------------------------------------------------
void Insert(Key v) 向优先队列中插入一个元素
Key max() 返回最大元素
Key delMax() 删除并返回最大元素
boolean isEmpty() 返回队列是否为空
int size() 返回优先队列中的元素个数
-------------------------------------------------------------------------------------------------------------
上面的为MaxPQ的API,与MaxPQ类似,我们也可以一个MinPQ的API,含有一个delMin()方法来删除并返回队列中键值最小的那个元素,只需要改变less()的方向就可以了。
1.堆的定义
在二叉堆的数组中,每个元素都要保证大于等于另两个特定位置的元素,也就是父节点一定要大于等于它的两个子节点,此时成为堆有序,由此可以退出,根节点是对有序的二叉树中的最大节点。
2.二叉堆表示方法
我们将二叉树的节点按照层级顺序放入数组中,根节点在位置1,它的子节点在位置2和3,而子节点的子节点分别在位置4,5 6,7以此类推。简单起见,下文的二叉堆我们简称为堆。在一个对中位置k的节点的父节点为[k/2](向下取整),而它的两个子节点位置分别为2k, 2k+1,这样在不适用指针情况下就在数组中实现了二叉堆。
一颗大小为N的完全二叉树的高度为[lgN] 下取整 (2 ^ h= N ,所以h=lgN)
3.堆的算法
我们用长度为N+1的私有数组pq[]来表示大小为N的对(N+1的原因是为了满足二叉树的结构关系,不会用到pq[0])堆元素放在pq[1]到pq[N]中。在排序算法中我们只通过less()和exchange()来访问元素,因为元素都在pq[]中,所以此时传入的参数不像之前博客的几篇排序算法中的less()和exch()传入整个数组,仅仅传入索引,具体实现如下
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
private void exch(int i, int j) {
Key temp = pq[i];pq[i] = pq[j];pq[j]=temp;
}
(1)由下至上的堆有序化(上浮)
当插入一个元素时,我们一般先放到数组的末尾,然后让它一点点向上移动,直到移动到它的父节点比他大,这种操作叫上浮swim(),这样一趟后,数组重新恢复有序性
rivate void swim(int k) {
while(k <=1 && less(k/2, k)){ //不断的向上移动,直到已经是最高父节点或他的父节点比他大
exch(k/2, k);
k = k/2;
}
}
(2)由下至上的堆有序化(下沉)
一般删除最大键值的方式是将最大键值与数组的末尾进行交换,然后把最大键值的指向null,可是由于数组的末尾肯定不是最大的父节点,所以需要将它一点点向下移动,直到移动到它的子节点比它小为止,这种操作就叫下沉 sink(),这样一趟后,数组重新恢复有序性
private void sink(int k) {
while(k*2 <= N){ //有两种跳出循环方式,一种是已经到底了,另一种就是下面的不在比他小
int j = k * 2;
if(j < N && less(j, j+1)) j++; //一般让这个节点和他子节点的最大值比较,这样减少交换次数,所以j++
if(!(less(k, j))) break; //此处就是不比他小时跳出
exch(k ,j);
k = j;
}
}
其实可以把元素的上浮和下沉理解为一个公司,当来了一个新人(insert)时,就先把他放到最底层,然后发现他比别人好,那就一点点给他升级,知道上升到他的上级比他还好为止,这就是上浮,而一个公司的大boss走了,此时由于他的下级有两个人,不知道让谁上去好,为了平衡整个管理体系,大boss出了一招,他和最末尾的员工换位置,最末尾的员工显然胜任不了最高级的位置,所以他就一点点的向下移动,知道他的下级比他还差,这叫下沉。
整体的代码实现如下
public class MaxPQ<Key extends Comparable<Key>> {
private Key[] pq; //基于堆的完全二叉树
private int N = 0; //存储于pq[1...N]中,pq[0]没有用
public MaxPQ(int maxN){
pq = (Key[]) new Comparable[maxN +1];
}
public boolean isEmpty(){
return N == 0;
}
public int size(){
return N;
}
public void insert(Key k){
pq[++N] = k; //插入一个数组,N就加一
swim(N); //上浮,恢复堆的有序性
}
private void swim(int k) {
while(k <=1 && less(k/2, k)){
exch(k/2, k);
k = k/2;
}
}
private void exch(int i, int j) {
Key temp = pq[i];pq[i] = pq[j];pq[j]=temp;
}
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
public Key delMax(){
Key max = pq[1]; //从根节点取到最大元素,最高父节点
exch(1, N--); //让根节点和数组的最后一个节点交换,同时将N自减一
pq[N+1] = null; //将最大元素指向空
sink(1); //下沉,恢复堆的有序性
return max;
}
private void sink(int k) {
while(k*2 <= N){
int j = k * 2;
if(j < N && less(j, j+1)) j++;
if(!(less(k, j))) break;
exch(k ,j);
k = j;
}
}
}
4优先队列调用示例
问题:输入N个字符串,每个字符串都对应着一个整数,你的任务就是从中找出最大的(或最小的)M个整数(及其相关联的字符串),在某些禅境中输入的量可能非常巨大,甚至可以认为是无限的,解决这个方法如果先排序,然后在找,或者不断的替换最大元素,这种比较的代价会非常高昂,所以此时可使用优先队列。
表: 从N个输入中找到最大的M个元素所需要的成本
-------------------------------------------------------------------------------------------
增长的数量级
示 例 ---------------------------------------------------------------------
时 间 空 间
-----------------------------------------------------------------------------------------------------------------
排序算法的用例 NlogN N
调用简单实现的优先队列 NM M
调用基于堆实现的优先队列 NlogM M
-----------------------------------------------------------------------------------------------------------------
调用的示例如下:
从命令行输入一个整数M,从输入流种获得一系列字符串,输入流的每一行代表一个交易,这段代码调用了MinPQ并会打印数字最大的M行,当优先队列的大小超过M时就删掉其中最小的元素,处理完所有交易,优先队列中方正以增煦排列的最大的M个交易。
5.多叉堆
基于用数组表示的完全三叉树构造堆并修改相应的代码并不难,对应数组中1至N的N个元素,位置k的结点大于大于等于3k-1,3k,3k+1的结点,小于位于[(k+1)/3]下取整的结点。