前言
数据结构队列的学习中,我们知道队列是先进先出的。任务被提交到队列中,按照先进先出的原则
对各个任务进行处理。不过在现实的情况下,任务通常有着优先级的概念,例如短任务、管理员的操作
应该优先执行。这是使用队列就无法描述了,这种特殊的应用我们使用一种称之为优先队列(堆)的方式
来解决。
优先队列
和队列一样优先队列也支持入队和出队操作,不过区别在于优先队列的出队操作出队的是优先级最高
的元素,这里以元素的大小来判定任务的优先级,越小,优先级越高。优先队列的模型如下图:
基于这种优先队列的模型,我们可以多种方法对其进行实现,一种常用的方法就是使用链表以O(1)执
行 插入操作,,遍历最小元素并删除花费O(N)时间。基于优先队列的插入操作一般情况下多于删除操作这
一情况, 前者是一种较好的实现。
另外可以使用二叉查找树来实现,这样一来插入和删除操作都是O(logN),不过二叉树支持多种
操作用以实现优先队列未免有点杀猪用牛刀的感觉。 下面将介绍二叉堆实现优先队列。
二叉堆
二叉堆就结构性质上来说就是一个完全填满的二叉树,满足结构性和堆序性。结构性不必多说,
就是完全二叉树应该满足的树结构。堆序性指的是:父节点的键值总是大于或等于(小于或等于)任何
一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
最大堆:当父节点的键值总是大于或等于任何一个子节点的键值。
最小堆:当父节点的键值总是小于或等于任何一个子节点的键值。
上面图片直接沿用的维基百科上的,笔者懒得在自己做个图了。
结构性质
上面对二叉堆的结构性质略有提及,这里我们进行下详细说明。看看二叉堆是如何
实现的。
仔细观察上述完全二叉树的结构,我们可以发现的是对于任意一个位置i,其
结点的左孩子在2i的位置,右孩子在2i+1的位置上,基于上述的性质我们可以使用
数组来实现二叉堆。
由此二叉堆可以使用一个数组和一个代表当前堆的大小的整数组成。考虑到涉及到
元素大小的比较,该数组元素实现了Comparable接口。
public class BinaryHeap
{
/**
* Construct the binary heap.
*/
public BinaryHeap( )
{
this( DEFAULT_CAPACITY );
}
/**
* Construct the binary heap.
* @param capacity the capacity of the binary heap.
*/
public BinaryHeap( int capacity )
{
currentSize = 0;
array = new Comparable[ capacity + 1 ];
}
private static final int DEFAULT_CAPACITY = 100;
private int currentSize; // Number of elements in heap
private Comparable [ ] array; // The heap array
}
以上代码显示的是一个优先队列的架构。至于其提供的具体操作,我们先看看
二叉堆的堆序性。
堆序性
简单的说保证优先队列的删除操作快速执行是堆序性质,基于这个特点我们需要找出
最小元素,那么最小元素应该在根结点上,也就是最小堆。这样我们就能以常数时间执行
findMin()操作了。
二叉堆基本操作
基于二叉堆的堆序性,我们来看看二叉堆基本操作是如何实现的吧!
insert(插入)
根据优先队列的模型,二叉堆实现的优先队列应该具备插入操作,不过根据其对序性
具体的插入操作是如何进行的呢?看下面的演示:
二叉堆插入元素14的情况。
为将一个元素 X 插入到堆中,我们在下一个可用位置创建一个空穴,否则该堆将不是完全数。
如果 X 可以放在该空穴中而不破坏堆的序,那么插入完成。否则,我们把空穴的父节点上的元素
移入该空穴中,这样,空穴就朝着根的方向上冒一步。继续改过程直到 X 能被放入空穴中为止。
这种实现过程称之为上滤:新元素在堆中上滤直到找出正确的插入位置。
实现代码:
public void insert( Comparable x ) throws Overflow
{
//这里没有进行扩容处理
if( isFull( ) )
throw new Exception( );
// Percolate up
int hole = ++currentSize;
//上滤,首先找到插入位置,之后元素交换一次
for( ; hole > 1 && x.compareTo( array[ hole / 2 ] ) < 0; hole /= 2 )
array[ hole ] = array[ hole / 2 ];
array[ hole ] = x;
}
可以知道的是当插入的元素小于堆中所有的元素的时候,必须上滤到根,插入时间为O(logN)
deleteMin(删除最小元)
基于优先队列的模型,出队的应该是最小元,按照最小堆的堆序性,找出最小元十分容易,麻烦
的地方在于删除之后破坏了结构型,这是需要进行一些额外的操作。
当删除一个最小元时,要在根节点建立一个空穴。由于现在堆少了一个元素,因此堆中最后一
个元素 X 必须移动到该堆的某个地方。如果 X 可以直接被放到空穴中,那么 deleteMin 完成。
不过这一般不太可能,因此我们将空穴的两个儿子中比较小者移入空穴,这样就把空穴向下推了
一层。重复该步骤直到 X 可以被放入空穴中。因此,我们的做法是将 X 置入沿着从根开始包含
最小儿子的一条路径上的一个正确的位置。
演示过程如下:
首先我们删除根元素13,建立一个空穴,之后判断元素31是否可以放入这个空穴中,
明显不能,会破坏堆序性.
之后我们选择较小的左儿子放入空穴,同时空穴下滑一层,之后判断31是否置于空穴中
同上,26置于空穴中,空穴下滑一层,31可以置于空穴中,过程结束。
这一种操作过程称之为下滤:空穴一步步下滑.
源码实现:
public Comparable deleteMin( )
{
if( isEmpty( ) )
return null;
Comparable minItem = findMin( );
array[ 1 ] = array[ currentSize-- ];
percolateDown( 1 );
return minItem;
}
private void percolateDown( int hole )
{
int child;
Comparable tmp = array[ hole ];
for( ; hole * 2 <= currentSize; hole = child )
{
child = hole * 2;
if( child != currentSize &&
array[ child + 1 ].compareTo( array[ child ] ) < 0 )
child++;
if( array[ child ].compareTo( tmp ) < 0 )
array[ hole ] = array[ child ];
else
break;
}
array[ hole ] = tmp;
}
同样的这个操作在最坏的情况下为O(logN),平均而言也为O(logN).
优先队列其他操作
上面对二叉堆实现的优先队列的两个基本操作做了一些讲解。我们知道的是在将任务提交给
优先队列的时候有时候我们需要根据实际情况修改任务的优先级。
decreaseKey(降低关键字的值)
desreaseKey(p,m)操作降低在位置p处的值,降值幅度为正m,不过这种方式很可能破坏
堆序性,因此需要通过上滤操作进行调整。
这种方式能够动态的提高某个任务的优先级,使其在能够优先开始。
increaseKey(降低关键字的值)
与上个操作相反,降低任务的优先级。delete(删除)
删除堆中某个任务,不过必须先执行decreasekey(P,+ ∞),然后执行deleteMin操作
这种操作的任务并不是正常终止的,而是被用户终止的。
构造二叉堆
上述讲了二叉堆的方式实现优先队列。那么一个二叉堆又是如何构造的呢?
简单的我们可以认为它可以使用N个相继的insert操作来完成。每个insert最坏时间为O(logN)
则其构建时间为O(N)。
更为常用的算法是先保持其结构性,之后再通过检查每个位置,下滤操作使其满足堆序性。
一开始满足结构性,但是并不满足堆序性,我们在元素70的位置进行下滤操作。
代码实现情况如下:
public BinaryHeap( T[] items ){
currentSize = items.length;
array = (T[]) new Comparable[ (currentSize + 2) * 11 / 10 ];
int i=1;
for( T item : items ){
array[ i++ ] = item;
}
buildHeap();
}
private void buildHeap(){
for( int i = currentSize/2; i>0; i-- )
percolateDown( i );
}
完整源码:
package com.kiritor;
import java.util.Arrays;
public class BinaryHeap
{
public BinaryHeap( )
{
this( DEFAULT_CAPACITY );
}
public BinaryHeap( Comparable[] items ){
currentSize = items.length;
array = new Comparable[ (currentSize + 2) * 11 / 10 ];
int i=1;
for( Comparable item : items ){
array[ i++ ] = item;
}
buildHeap();
}
public BinaryHeap( int capacity )
{
currentSize = 0;
array = new Comparable[ capacity + 1 ];
}
public void insert( Comparable x )
{
// Percolate up
int hole = ++currentSize;
for( ; hole > 1 && x.compareTo( array[ hole / 2 ] ) < 0; hole /= 2 )
array[ hole ] = array[ hole / 2 ];
array[ hole ] = x;
}
public Comparable findMin( )
{
if( isEmpty( ) )
return null;
return array[ 1 ];
}
public Comparable deleteMin( )
{
if( isEmpty( ) )
return null;
Comparable minItem = findMin( );
array[ 1 ] = array[ currentSize-- ];
percolateDown( 1 );
return minItem;
}
private void buildHeap( )
{
for( int i = currentSize / 2; i > 0; i-- )
percolateDown( i );
}
public boolean isEmpty( )
{
return currentSize == 0;
}
public boolean isFull( )
{
return currentSize == array.length - 1;
}
public void makeEmpty( )
{
currentSize = 0;
}
private static final int DEFAULT_CAPACITY = 100;
private int currentSize; // Number of elements in heap
private Comparable [ ] array; // The heap array
private void percolateDown( int hole )
{
int child;
Comparable tmp = array[ hole ];
for( ; hole * 2 <= currentSize; hole = child )
{
child = hole * 2;
if( child != currentSize &&
array[ child + 1 ].compareTo( array[ child ] ) < 0 )
child++;
if( array[ child ].compareTo( tmp ) < 0 )
array[ hole ] = array[ child ];
else
break;
}
array[ hole ] = tmp;
}
// Test program
public static void main( String [ ] args )
{
int numItems = 50;
BinaryHeap h = new BinaryHeap( numItems );
int i = 37;
try
{
for( i = 37; i != 0; i = ( i + 37 ) % numItems )
h.insert( new Integer( i ) );
System.out.println(Arrays.toString(h.array));
System.out.println(h.findMin());
h.deleteMin();
System.out.println(Arrays.toString(h.array));
}
catch( Exception e )
{ System.out.println( "Overflow (expected)! " + i ); }
}
}
简单执行结果:
[null, 1, 2, 7, 6, 3, 12, 10, 9, 8, 5, 4, 13, 24, 22, 11, 34, 21, 19, 16, 27, 17, 15, 14, 25, 38, 31, 49, 36, 23, 18, 47, 48, 35, 42, 45, 46, 32, 29, 43, 40, 30, 37, 41, 33, 28, 20, 39, 44, 26, null]
1
[null, 2, 3, 7, 6, 4, 12, 10, 9, 8, 5, 14, 13, 24, 22, 11, 34, 21, 19, 16, 27, 17, 15, 20, 25, 38, 31, 49, 36, 23, 18, 47, 48, 35, 42, 45, 46, 32, 29, 43, 40, 30, 37, 41, 33, 28, 26, 39, 44, 26, null]
By Kiritor
2013 /06 /16 父亲节