前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >优先队列(堆)

优先队列(堆)

作者头像
zy010101
发布2019-05-25 19:55:28
3640
发布2019-05-25 19:55:28
举报
文章被收录于专栏:程序员程序员

优先队列:顾名思义,这个队列中的元素是有优先级的,它和普通的先进先出(FIFO)不一样。我们在很多场合可能都需要用到这种特殊的队列(例如,操作系统的进程调度)。

可以看出来,优先队列(priority queue)的核心操作有两个,分别是插入和删除。插入是显而易见的,删除就是找出这个队列中优先级最高的那个元素(可以是最大的,也可是最小的)。

一些简单的想法:我们可以采用一个简单的链表来实现,表头始终存放优先级最高的元素,这样删除操作的时间复杂度就是O(1),那么相应的插入操作就是O(n)。反过来,插入在表头进行,删除是找出优先级最高元素,这样插入就是O(1),删除就是O(n)。这两种想法都不能得到更好的时间复杂度了。另外,使用BST也可以实现这种操作,但是这通常会使的树变成一颗斜树。导致树的深度很深,也不怎么好用。

二叉堆:完全二叉树经常被用来实现优先队列,因此以至于堆(heap)不加修饰的出现的时候,都是指优先队列这种数据结构。完全二叉树的高度是O(log n)。它非常平衡。这点很重要。另外还有一点就是完全二叉树表现的很有规律。当使用一个数组来保存完全二叉树的时候,从下标为1的地方开始按照层序遍历的方式存储完全二叉树,那么下标为i的节点,它的左儿子在下标为2i的地方保存着,右儿子在下标为2i+1的地方保存着。唯一的缺点是最大的堆的大小需要事先有一个良好的估计。下图是一棵完全二叉树在数组中存储的关系。

我们想快速找出优先级最高的元素,那么优先级最高的放在根上。如果考虑任意的子树也是堆,那么任意节点的优先级都应该高于它的所有后裔。这就是堆序性。在这里我们的堆删除最小元素。因此根节点将比它所有的后裔都小,任意节点也应该小于它所有的后裔。下面给出堆的ADT。

代码语言:javascript
复制
#ifndef HEAP
#define HEAP

#include<stdio.h>
#include<stdlib.h>
#include<limits.h>

struct HeapNode
{
	int *Heap;
	int size;			//当前已用容量
	int capacity;			//总容量
};
typedef struct HeapNode *priorityqueue;

priorityqueue InitHeap(int max);		//初始化堆,需要给出堆的最大容量
void DestoryHeap(priorityqueue H);		//free掉堆
void Insert(priorityqueue H,int x);	        //核心操作,插入
int FindMin(priorityqueue H);			//这是个附加操作,很容易实现
int DeleteMin(priorityqueue H);			//核心操作,删除最小元
int IsFull(priorityqueue H);			//数组实现的弊端,必须判断满不满
int IsEmpty(priorityqueue H);			//是否空

#endif // !HEAP

首先,给出堆的初始化代码,注意,这里是用数组实现的堆。

代码语言:javascript
复制
priorityqueue InitHeap(int max)
{
	priorityqueue H = NULL;
	H = (priorityqueue)malloc(sizeof(struct HeapNode));
	if (NULL == H)
	{
		printf("创建堆失败!");
		exit(0);
	}
	else
	{
		H->Heap = (int *)malloc(sizeof(int)*(max + 1));	//因为从下标为1开始放
		if (NULL == H->Heap)
		{
			printf("创建堆失败!");
			exit(0);
		}
		else
		{
			H->Heap[0] = INT_MIN;    //限位器作用
			H->capacity = max;
			H->size = 0;		//初始化没有元素
		}
	}
	return H;
}

插入操作:堆的两种基本操作,删除和插入的实现都是简单的,只需要始终保持堆序性即可。首先,插入是在下一个空闲位置创建一个空穴(保证它是完全二叉树)。如果X放在这个空穴处不影响堆序性,那么插入完成。否则将空穴父节点上的元素移动到空穴。一直这样下去,空穴就一直向树根方向移动。直到X能放入该空穴,而不影响堆序性为止。

代码语言:javascript
复制
void Insert(priorityqueue H, int x)
{
	if (IsFull(H))
	{
		printf("堆已满,无法插入!");
        //另一种处理方式是realloc()
	}
	else
	{
		//++H->size,创建空穴,判断是否影响堆序性。
		int i;
		for (i = ++H->size; H->Heap[i / 2] > x; i /= 2)
		{
			H->Heap[i] = H->Heap[i / 2];	//若影响,将父节点的值复制到空穴。
		}
		H->Heap[i] = x;					//若不影响堆序性,则将元素放入空穴。
	}
}

需要说明的是,在H->Heap[0]中,放入了一个极小的元素,保证x不会比他更小。那么我们就省去了如果新插入的元素是最小值的时候,当i == 1时就要跳出循环的判断了。这就是限位器的作用。这种想法类似于链表添加一个头结点。省去了这个判断,因此程序也能更快点。这种插入在最坏情形下需要O(log n),在平均情形下则要好得多。

删除操作:DeleteMin类似于插入操作,当删除最小元素的时候,那么根将为空。我们需要一个新根。这个新根可以取左右子儿子中小的那一个,这样空穴就被向下退了一层,接着继续比较空穴的左右儿子,选其中小的那个放入空穴,将空穴继续下推,直到将堆中最后一个元素放入空穴为止。

代码语言:javascript
复制
int DeleteMin(priorityqueue H)
{
	int i, child;
	int min, last_element;

	if (IsEmpty(H))
	{
		printf("堆为空,无法执行删除操作!");
		return H->Heap[0];		//返回这个极限值
	}

	min = H->Heap[1];			//保存最小值
	last_element = H->Heap[H->size];   
	//下滤
	for  (i = 1; 2 * i <= H->size ; i = child)		//i每次更新为下一个空穴的位置
	{
		child = 2 * i;		//左儿子的位置
		if (child != H->size && H->Heap[child] > H->Heap[child + 1])	
		{	//这里需要保证child有另外一个兄弟,所以判断child != H->size
			child++;		//找出更小的孩子节点
		}
		if (last_element > H->Heap[child])	//若最后一个元素比这个最小元素还大,那么接着下滤
		{
			H->Heap[i] = H->Heap[child];
		}
		else		//最后一个不比这个小元素大,那么将其放入空穴,删除完毕。
		{
			H->Heap[i] = last_element;
			break;
		}
	}
    H->size--;    //堆中元素数目减1
	return min;
}

删除的时间复杂度是O(log n)。求最小元的堆在求最大元的时候没有任何帮助,唯一知道的信息是最大元在叶子节点上。但是如果一棵树很大的话,这个信息也没有任何帮助,因为,此时近乎一般的元素在树叶上。所以想知道最大值很困难。

我们可以通过插入操作来构建一个堆,但是这样的时间复杂度高达O(n logn).因此一般的构建堆的方法是将N个元素直接放入数组中,形成一个完全二叉树。然后把这个不满足堆序性的完全二叉树改造成堆。代码实现如下。

代码语言:javascript
复制
priorityqueue BuildHeap(priorityqueue H)
{
	int temp, i = 1;
	int start = 1;
	while (EOF != scanf("%d",&temp))
	{
		H->Heap[i++] = temp;		//无序数组
	}
	H->size = i - 1;		//堆中元素个数
	int parent, child;
	int x;
	for ( i = H->size / 2; i > 0; i--)	//从最后一个元素的父节点开始构建堆
	{
		//考虑到最后一个元素可能没有兄弟,也可能有兄弟。
		 x = H->Heap[i];
		for (parent = i; parent * 2 <= H->size; parent = child) 
		{
			child = parent * 2;		//左儿子一定存在
			if ((child != H->size) && (H->Heap[child] > H->Heap[child + 1]))
			{
				child++;  //找到其中最小的那个儿子
			}
			if (x <= H->Heap[child])		//满足堆序性
			{
				break; //找到合适位置
			}
			else			//下滤
			{
				H->Heap[parent] = H->Heap[child];
			}	
		}
		H->Heap[parent] = x;
	}
	return H;
}

堆的其他操作都很简单。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年10月29日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档