您的位置:首页 > 理论基础 > 数据结构算法

数据结构之---优先队列(堆)

2014-08-27 23:06 253 查看
学过操作系统的应该都知道,在进程管理中进程调度的一种策略是基于优先级的调度,这个时候传统的队列数据结构便不支持了,所以引入了新的数据结构---堆。它支持插入以及删除返回最小元(当然也可以定义为其它操作)的基本操作。一个堆是一棵完全二叉树,常见的有最小堆和最大堆,最小堆是所有节点都比其子树的节点小,最大堆相反。利用最小堆的这种特性,它还往往被用到排序中,称为堆排序。以完全二叉树支持的堆,对于以上两个动作以O(logN)的最坏情形支持。插入实际上将花费常数平均时间。利用完全二叉树的特性,这颗树可以不用指针,而直接采用数组实现,一棵高h的完全二叉树有2^h---2^(h+1)-1个节点。对任意节点i,其做孩子在2i上,右孩子在2i+1上。

下面给出其基本的数据结构和相应的支持:

struct heapstruct;
typedef struct heapstruct *heap;
struct heapstruct
{
int capacity;
int size;
elementtype *element;
};
heap init(int maxelement);
void insert(elementtype x,heap h);/*往堆里插入一个元素*/
void delete_min(heap h);/*删除最小元素*/
void percolate_down(heap h,int i);/*节点的下滤*/
int is_full(heap h);
int is_empty(heap h);

首先是其初始化操作:

heap init(int maxelement)
{
heap h;
h=(heap)malloc(sizeof(struct heapstruct));
if(h==NULL)
fatalerror("out of space");
h->element=(element*)malloc(sizeof(elementtype)*maxelement);
if(h->element==NULL)
fatalerror("out of space");
h->size=0;
h->capacity=maxelement;
return h;
}然后是判断其是否为空或者为满的操作:
int is_full(heap h)
{
return (h->capacity==h->size)?1:0;
}

int is_empty(heap h)
{
return (h->size==0)?1:0;
}
我们来考察怎样往堆里插入一个元素,首先我们将堆增大一个位置,当然这个元素不一定是插入到这里的,由于堆序性的原因我们将这个这个元素依次跟它的父亲做比较,如果其父节点比它大,那么我们将这个空白节点往上滤,也就是说将父节点赋值下来,直到父节点小于该节点为止,最后将这个元素插入到这个空位中。这中操作叫做上滤。注意到当中有一个技巧是我们并没有立即将元素进行赋值然后采取交换(这将会消耗3个操作),而是直到最后才予以赋值。这个小小的技巧可以节约一些时间,在后面的排序中还将看到这种操作。
注意到一个细节是,数组的0号元素并没有被用到。在此处可以采取的一个小技巧是将0号元素赋一个比所有可能元素都小的值,这样在上滤操作中,最极端的情况是在1号位置停止,即刚好是根节点。当然我们也可以不采用此策略,而添加额外的控制条件。

一个insert操作是类似下面这样的:

void insert(elementtype x,heap h)/*采用上滤的插入,此处注意到的是我们0号元素并没有使用*/
{
if(is_full(h))
fatalerror("queue is full!");
int i;
for(i=++h->size;i>=2;i/=2)/*此处注意,我们同时将size增大了1,此处其实还有一个处理方法,就是将0号元素置得比所有可能值都小,那么元素最多会在1出停下*/
{
if(h->element[i/2]>x)
h->element[i]=h->element[i/2];
else
break;
}
h->element[i]=x;
}

在删除之前我们先来考虑其中的一个小操作。我们知道删除是将第一个元素弹出,此时堆的大小便会减小1。需要有新的元素填到堆顶来。一个容易想到的操作是我们将最后一个元素拿出来,然后将最后一个节点删除(因为这个删除相当容易,不会影响到其它节点),然后再为它找到一个合适的位置。这相当于将该节点插入到头节点然后进行调整。在调整过程中我们总是把儿子节点中较小的那个放到父节点上来,直到元素值比它们都小为止,然后将元素插入到这个位置,一次删除便完成了。注意到插入点的向下移动,这种操作叫做下滤。由于其重要性,以及在后面还有用处,将它单独写出来:
void percolate_down(heap h,int i)
{
elementtype temp=h->element[i];
int k;
int child;
for(k=i;k*2<=h->size;k=child)
{
child=k*2;
if(child!=h->size&&h->element[child+1]<h->element[child])/*注意此处的判断,我们总是小心*/
child++;
if(h->element[k]>h->element[child])
h->element[k]=h->element[child];
else
break;
}
h->element[k]=temp;
}
该函数的作用是将i节点的位置调节到合适的位置上去,当然如果它本身就是符合堆序的将没有调节操作。注意到的是这里用到的还是前面的技巧,我们并不直接交换值,而是采取直接向上赋值,最后再将节点填入到节点中。在判断左右儿子大小的时候,一个额外的小注意是为了防止数组下标越界,我们总得判断一下,毕竟再小心都不为过。
接下来便是删除操作了,它得到了上面下滤操作的支持,我们把最后一个节点的值放在根节点,然后进行下滤操作以完成一次删除操作:

void delete_min(heap h)
{
if(is_empty(h))
fatalerror("queue is empty!");
elementtype minelement=h->element[1];/*此处注意*/
elementtype lastelement=h->element[h->size--];/*取得最后一个元素,同时将size减小1*/
h->element[1]=lastelement;/*将最后一个节点赋给第一个节点,同时进行调节*/
percolate_down(h,1);
return minelement;
}还有一个我们关心的操作这里没有给出,即我们怎样利用一串数据建立一个初始的堆。一个最简单明了的操作当然是我们不断的执行insert操作就行了。还有一种方法是我们首先这些数据随机的放入到树中,只要保持其完全二叉树的结构性即可,然后我们从拥有儿子的节点(即N/2)开始进行下滤操作,即对它们进行调整,最后也能够建立一个堆。它的操作看起来是这样的:
for(i=N/2;i>0;i--)
percolate_down(h,i);对于构建一棵树操作的操作中,一个insert操作将花费O(1)的平均时间以及O(logN)的最坏时间,应此应该注意的是该算法运行的时间是O(N)平均时间而不是O(NlogN)最坏情形。第二种操作的平均也是O(N)。即我们建立一棵树将花费线性的平均时间。当然其最坏的时间将是O(NlogN)。
对insert与delete操作的最坏运行时间均是O(logN)。并且delete操作看起来运行的效果总要差一点,其平均时间也是O(logN),而insert操作的平均时间是O(1)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  堆排序