随笔-156  评论-223  文章-30  trackbacks-0
前言
   众所周知,stl中的优先级队列是基于最大堆实现的,能够在对数时间内插入元素和获取优先级最高的元素,但如果要求在对数时间内还能获取优先级最低的元素,而不只是获取优先级最高的元素,该怎么实现呢?可以用最大堆-最小堆或双端堆数据结构来实现,最大堆-最小堆和双端堆都是支持双端优先队列的插入、删除最小元素和最大元素等操作的堆,在这些操作上,时间复杂度都是对数时间,但是双端堆的操作比最大堆-最小堆的相应操作还要快一个常数因子,而且算法更加简单,因此本文讲述选择使用双端堆实现优先级队列的原理。

定义与性质
   双端堆是一颗完全二叉树,该完全二叉树要么为空,要么满足以下性质:
 (1)根结点不包含元素
 (2)左子树是一个最小堆
 (3)右子树是一个最大堆
 (4)如果右子树不为空,则令i是左子树中任一结点,j是i在右子树中的对应结点,如果i在右子树中的对应结点不存在,则j为i的父结点在右子树中的对应结点,  对于结点i和j,i的关键字值小于等于j的关键字值。
   从以上性质可以推出:对于左子结中的任一结点i,设j为i的对称结点,则由最小元素到i,i到j,j到最大元素的路径上元素是非递减有序的。在双端堆上的操作算法核心就是为了保证这样的单向路径上元素必须是非递减有序的。
   例如下图所示,就是一个双端堆
操作算法
 (1)插入元素:设所插结点为i,其对称结点为j,要分2种情况 a)当i处于最小堆时,则j处于最大堆中,如果KeyValue(i)>KeyValue(j),则设置value= KeyValue(i),KeyValue(i)=KeyValue(j),并执行在最大堆中位置j处插入值value的操作;否则执行在最小堆中位置i处插入值KeyValue(i)的操作。b)当i处于最大堆时,则j处于最小堆中,如果KeyValue(i)<KeyValue(j),则设置value=KeyValue(i),KeyValue(i)=KeyVaue(j),并执行在最小堆中位置i处插入值value的操作;否则执行在最大堆中位置j处插入值KeyValue(i)的操作。

 (2)删除最小元素:首先在最小堆中执行一个向下回溯过程,这个过程执行的堆大小要比原来的堆小1,从最小元素结点开始,每次选取关键字值较小的孩子结点,并用其值更新父结点值,直到底部没有孩子结点,执行的结果是在底部某叶子结点i处形成一个空洞(形象说法,这个空洞需要后续操作来调整填补,下同),i的对称结点j在最大堆中,设最末元素结点为e,如果KeyValue(e)>KeyValue(j),则设置KeyValue(i)=KeyValue(j),并执行在最大堆中位置j处插入值KeyValue(e)的操作;否则执行在最小堆中位置i处插入值KeyValue(e)的操作。

 (3)删除最大元素:这个操作比删除最小元素要麻烦一点,和删除最小元素类似,先执行一个向下回溯过程得到空洞结点i,其对称结点为j,为了保证能满足双端堆的性质,需要考虑以下几种情况:a)j只存在一个孩子,如下图第1个所示。 b)j存在两个孩子,如下图第2个所示。 c)j不存在孩子,但存在左兄弟(可能存在孩子),如下图第3个所示。 d)j不存在孩子,但存在右兄弟,如下图最后一个所示。
令min为具有较小值的结点,max为具有较大值的结点,最末元素结点为e,value=KeyValue(e),如果j存在孩子结点,则 min为孩子中关键字值较小的结点,max为关键字值较大的结点;否则min为j和其兄弟结点中关键字值较小的结点,max为关键字值较大的结点。如果KeyValue(min)<value而且value<KeyValue(max),在这种情况下,只需调整i或其父结点、max的值即可,操作如下:如果KeyValue(i)<KeyValue(max),则设置KeyValue(parent(i))=KeyValue(max),否则设置KeyValue(i)=KeyValue(max),最后设置KeyValue(max)=value;如果KeyValue(max)<=value,在这种情况下,则执行在最大堆中位置i处插入值value的操作;如果value<=KeyVlaue(min),在这种情况下,先调整i或其父结点、max的值(见上),再执行在最小堆中位置min处插入值value的操作。

 (4)构造堆:给定一个元素大小为N的序列S,在这个序列空间上构造堆,一般有两种实现方法,一是循环N-1次,每次插入元素S[i],也就是自顶向下构建堆,时间复杂度为O(NlgN)。另一种是从最后一个内部结点N/2左右开始,执行调整堆操作,直到第一个元素,也就是自底向上构建堆,时间复杂度为O(N)。

 (5)最大堆(最小堆)插入:这是一个向上回溯过程,和单个最大堆(最小堆)操作是一样的,从底部某处开始,比较待插元素和结点关键字值大小,直到前者不大于(不小于)后者时或碰到最大堆(最小堆)顶部为止,这时就找到了插入位置,将待插元素放到这个位置即可。

   设双端堆元素个数为N,由于完全二叉树高度为O(lgN),则插入元素操作时间复杂度为O(lgN),删除最小元素和最大元素为不超过2O(lgN),实现这些操作最重要的一点就是要保证性质4,只有当性质4满足时,双端堆才有意义,才能保证获取最大元素和最小元素操作的对数时间,具体的实现详见下文。
posted on 2011-10-03 17:53 春秋十二月 阅读(3747) 评论(3)  编辑 收藏 引用 所属分类: Algorithm

评论:
# re: 基于双端堆实现的优先级队列(2): 内幕 2014-01-05 10:06 | Cppowboy
你好,我学习了您讲解的算法之后自己按照算法写了C++的实现,包括插入,删除最小,删除最大的操作,但是在线评测总是出问题,个人检查认为是对的,您能帮忙指点一下,看看哪里错了吗?
#include <stdio.h>
#include <cstring>
#include <cmath>
#include <stdlib.h>
#include <iostream>
using namespace std;
#define SIZE 110100

struct heap
{
public:
void init();
void insert(long long);
long long del_min();
long long del_max();

void fool_min_insert(long long,long long);
void fool_max_insert(long long,long long);
void min_push_down(long long&);
void max_push_down(long long&);

long long parent(long long n){return n/2;}
long long lchild(long long n){if(n*2<=size)return 2*n;else return 0;}
long long rchild(long long n){if(n*2+1<=size)return n*2+1;else return 0;}
bool is_min_heap(long long n)
{
while(n>3)n/=2;
if(n==2)return true;
else return false;
}
long long partner(long long n)
{
long long j=log((double)n)/log((double)2);
long long k=(long long)pow((double)2,(double)j)-1;
long long e=k^n;
if(e<=size)return e;
else return partner(parent(n));
}
void swap(long long &a,long long &b){long long temp=a;a=b;b=temp;}
private:
long long key[SIZE];
long long size;
};
void heap::init()
{
size=1;
memset(key,-1,sizeof(key));
}
void heap::fool_min_insert(long long k,long long pos)
{
key[pos]=k;
long long cur=pos;
while(parent(cur)>1)
{
if(key[parent(cur)]>key[cur])
{
swap(key[parent(cur)],key[cur]);
cur=parent(cur);
}
else break;
}
}
void heap::fool_max_insert(long long k,long long pos)
{
key[pos]=k;
long long cur=pos;
while(parent(cur)>1)
{
if(key[parent(cur)]<key[cur])
{
swap(key[parent(cur)],key[cur]);
cur=parent(cur);
}
else break;
}
}
void heap::insert(long long k)
{
size++;
if(size==2)
{
key[size]=k;
return;
}
if(is_min_heap(size))
{
long long j=partner(size);
if(k<=key[j])
fool_min_insert(k,size);
else
{
fool_min_insert(key[j],size);
fool_max_insert(k,j);
}
}
else
{
long long j=partner(size);
if(k>=key[j])
fool_max_insert(k,size);
else
{
fool_max_insert(key[j],size);
fool_min_insert(k,j);
}
}
}
void heap::min_push_down(long long& cur)
{
long long c;
while(lchild(cur)!=0)
{
c=cur;
if(lchild(cur)!=0)c=lchild(cur);
if(rchild(cur)!=0&&key[rchild(cur)]<=key[lchild(cur)])
c=rchild(cur);
key[cur]=key[c];
cur=c;
}
}
void heap::max_push_down(long long& cur)
{
long long c;
while(lchild(cur)!=0)
{
c=cur;
if(lchild(cur)!=0)c=lchild(cur);
if(rchild(cur)!=0&&key[rchild(cur)]>=key[lchild(cur)])
c=rchild(cur);
key[cur]=key[c];
cur=c;
}
}
long long heap::del_min()
{
if(size==1)return 0;
if(size==2)
{
size--;
return key[size+1];
}
long long e=key[2];
long long cur=2;
min_push_down(cur);
long long j=partner(cur);
long long value=key[size];
if(value<=key[j])
fool_min_insert(value,cur);
else
{
fool_min_insert(key[j],cur);
fool_max_insert(value,j);
}
size--;
return e;
}
long long heap::del_max()
{
if(size==1)return 0;
if(size==2)return key[size--];
long long e=key[3];
long long cur=3;
max_push_down(cur);
long long j=partner(cur);
long long value=key[size];
if(lchild(j)==size)j=lchild(j);
else if(rchild(j)<=size)
j=(key[lchild(j)]>key[rchild(j)])?lchild(j):rchild(j);
if(value>=key[j])
fool_max_insert(value,cur);
else
{
fool_max_insert(key[j],cur);
fool_min_insert(value,j);
}
size--;
return e;
}
int main()
{
heap h;
h.init();
long long n,pts;
char ch;
scanf("%lld",&n);
while(n-->0)
{
getchar();
scanf("%c",&ch);
if(ch=='I')
{
scanf("%lld",&pts);
h.insert(pts);
}
else if(ch=='H')
printf("%lld\n",h.del_max());
else if(ch=='L')
printf("%lld\n",h.del_min());
}
return 0;
}  回复  更多评论
  
# re: 基于双端堆实现的优先级队列(2): 内幕[未登录] 2014-01-05 16:02 | 春秋十二月
@Cppowboy
是不是操作后,序列违反了双端堆的性质?我的代码是可以用的,当时做过很久的随机测试,都没有违反性质。  回复  更多评论
  
# re: 基于双端堆实现的优先级队列(2): 内幕[未登录] 2014-01-05 16:11 | 春秋十二月
@Cppowboy
实现细节可以不同,我的实现与stl中的优先级队列类似,关键是各种操作后,保证序列不违反性质就行了。  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理