从优先队列到二叉堆

来源:互联网 发布:淘宝嘉年华怎么报名 编辑:程序博客网 时间:2024/05/19 00:42

什么是优先队列

优先队列:顾名思义 ,首先他要是一个队列。我们先来回顾一下普通队列的性质:普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。但是优先队列前面加上了”优先”两个字,所以优先队列是根据元素的属性选择某一项值最优的出队,即具有最高优先级的元素最先删除。
优先队列是允许至少下列两种操作的数据结构:insert(插入),它的作用是显而易见的;deleteMin(删除最小者),它的工作是找出、返回并删除优先队列中最小的元素。insert等价于enqueue,而deleteMin则是队列运算dequeue.

一些简单的实现

基于这种优先队列的模型,我们可以多种方法对其进行实现,一种常用的方法就是使用链表以O(1)执行插入操作,遍历最小元素并删除花费O(N)时间。另一种方法是始终让链表保持排序的状态;这使得插入代价高昂O(N),而删除花费低廉O(1)。基于优先队列的插入操作一般情况下多于删除操作这一情况, 前者是一种较好的实现。
另外可以使用二叉查找树来实现,这样一来插入和删除操作都是O(logN),不过二叉树支持多种操作用以实现优先队列未免有点杀猪用牛刀的感觉。 下面将介绍二叉堆实现优先队列。

二叉堆

我们即将使用的这个工具二叉堆,我们简称它为堆。与二叉查找树一样,堆也有两个重要的性质:即结构性和堆序性。对堆的一次操作可能破坏这连个性质中的一个,因此堆的操作必须到堆的所有性质都被满足时才能被终止。

结构性质

堆是一颗被完全填满的二叉树,有可能的例外是在底层,底层的元素从左到右依次填入。我们称这样的树为完全二叉树。
这里写图片描述
容易证明,一棵高为h的完全二叉树有2的k次方到2的(k+1)次方-1个节点,这意味着数的┖logN┚(向上取整)。我们还可以发现如下规律
这里写图片描述
对于数组中任一位置i上的元素,其左儿子在位置2*i* 上,右儿子在左儿子后的单元(2*i*+1)中,它的父亲则在位置┖i/2┚上。因此,这里不仅不需要链,而且遍历该树所需要的操作极简单。这种方法的唯一问题在于,最大的堆大小需要事先估计。

public class BinaryHeap{    public BinaryHeap( int capacity )    {        currentSize = 0;        array = new Comparable[ capacity + 1 ];    }    private int currentSize;  // Number of elements in heap    private Comparable [ ] array; // The heap array}

堆序性质

让操作快速执行的性质是堆序性质。由于我们想要快速找出最小元,因此最小元应该在根上。如果我们考虑任意子树也是一个堆,那么任意节点就应该小于它的所有后裔。因而得到如下性质:对于每一个节点XX 的父亲中的关键字小于(或等于)X 中的关键字,根节点除外(他没有父节点)。根据堆序性质,最小元素总可以在根处找到。因此,我们以常数时间得到附加操作findMin。

基本的堆操作

insert
为将一个元素X 插入到堆中,我们在下一个可用位置创建一个空穴,否则该堆将不是完全树。如果X 可以放在该空穴中而并不破坏堆的序,那么完成插入。否则,我们把空穴的父节点上的元素移入该空穴中,这样空穴就朝着根的方向上冒一步。继续该过程直到X 能被放入到空穴中为止。这种一般的策略叫做上滤(percolate up)。
这里写图片描述
这里写图片描述
实现代码:

public void insert( Comparable x ) throws Overflow{ //这里没有进行扩容处理 if( isFull( ) )    throw new Exception( ); // Percolate up   int hole = ++currentSize; //上滤,首先找到插入位置,之后元素交换一次   for( array[0] = x;x.compareTo( array[ hole / 2 ] ) < 0;        hole /= 2 )            array[ hole ] = array[ hole / 2 ];   array[ hole ] = x;        }

可以知道的是当插入的元素小于堆中所有的元素的时候,必须上滤到根,插入时间为O(logN)
deleteMin
deleteMin以类似插入的方式处理。找到最小元是容易的,困难的是删除它。当删除一个最小元时,在根节点处就创建了一个空穴。因此堆中最后一个元素X 必须移动到该堆的某个地方。如果X 可以被放到空穴中,那么deleteMin完成。不过这一般不太可能,我们将空穴的两个儿子中较小的放入空穴,这样就把空穴向下推了一层。重复该步骤直到X 可以被放入到空穴中。
这里写图片描述
首先我们删除根元素13,建立一个空穴,之后判断元素31是否可以放入这个空穴中,明显不能,会破坏堆序性.
这里写图片描述
之后我们选择较小的左儿子放入空穴,同时空穴下滑一层,之后判断31是否置于空穴中
这里写图片描述
同上,26置于空穴中,空穴下滑一层,31可以置于空穴中,过程结束。
这一种操作过程称之为下滤(percolate down),空穴一步步下滑.
同样的这个操作在最坏的情况下为O(logN),平均而言也为O(logN).

其他的堆操作

decreaseKey(降低关键字的值)
desreaseKey(p,m)操作降低在位置p处的值,降值幅度为正m,不过这种方式很可能破坏堆序性,因此需要通过上滤操作进行调整。这种方式能够动态的提高某个任务的优先级,使其在能够优先开始。
increaseKey(增加关键字的值)
与上个操作相反,降低任务的优先级。
delete(删除)
删除堆中某个任务,不过必须先执行decreasekey(P,+ ∞),然后执行deleteMin操作。这种操作的任务并不是正常终止的,而是被用户终止的。

构造二叉堆

上述讲了二叉堆的方式实现优先队列。那么一个二叉堆又是如何构造的呢?简单的我们可以认为它可以使用N个相继的insert操作来完成。每个insert最坏时间为O(logN),则其构建时间为O(N)。
更为常用的算法是先保持其结构性,之后再通过检查每个位置,下滤操作使其满足堆序性。

这里写图片描述
按照上述交换规则,以此类推。就可构建二叉堆。

二叉堆的不足

根据上面的分析,二叉堆的insert复杂度O(logN),deleteMin最坏也是O(logN)。但是如果需要查找堆中某个元素呢?或者需要合并两个堆呢?
对于二叉堆而言,对find 和 merge操作的支持不够。这是由二叉堆的存储结构决定的,因为二叉堆中的元素实际存储在数组中。正因为如此,所有支持有效合并的高级数据结构都需要使用链式数据结构。

0 0
原创粉丝点击