十四、【优先级队列】完全二叉堆(complete binary heap)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_18108083/article/details/85203134

在实际环境中,普通的FIFO的队列不能保证整体效率达到最高,如CPU的多任务调度,医院诊疗等,简单的“先入先出”原则并不是很好的方案,对大多数问题而言,应该根据事物特性的指定合适的优先级方案灵活的操作,而优先级队列很适合处理这样的问题。

一、优先级队列

此前的搜索树结构和词典结构,都支持覆盖数据全集的访问和操作,它们定义并维护显式或者隐式的全序关系(full order)。就对外接口的功能而言,优先级队列,较之此前的数据结构反而有所削弱。

优先级队列将操作对象限定于当前的全局极值者。比如,在全体北京市民中,查找年龄最长者,或者在所有鸟类中,查找种群规模最小者,等等。这种根据数据对象之间相对优先级对其进行访问的方式,与此前的访问方式有着本质区别,称循优先级访问(call-by-priority)。优先级队列并不动态维护整个容器内元素的全序,却转而维护一个偏序(partial order) ,其高明之处在于,如此不仅足以高效地支持仅针对极值对象的接口操作,更可有效地控制整体计算成本。

优先级队列:按照事先约定的优先级,可以始终高效查找并访问优先级最高数据项的数据结构,统称为优先级队列(priority queue)

二、堆

基于列表或向量等结构的实现方式,之所以无法同时保证insert()和delMax()操作的高效率,原因在于其始终都保存了全体词条之间的全序关系。实际上,尽管优先级队列的确隐含了“所有词条可相互比较”这一条件,但从操作接口层面来看,并不需要真正地维护全序关系,只要能够确定全局优先级最高的词条即可。

事实上,此前的向量、列表等数据结构,都不能同时保证插入、删除、取优先级最大的元素这三个接口的时间复杂度均在O(logn)范围内,虽然平衡二叉树中的AVL树、伸展树、红黑树可以做到,但是他们的功能对于实现优先级队列来说功能过于强大,可谓是杀鸡用牛刀,而利用堆实现的优先级队列则是一种轻量级数据结构,从实现成本上来看更加适合!

三、完全二叉堆

堆有多种实现形式,其中最基本的一种形式即为完全二叉堆(complete binary heap)。完全二叉堆应满足两个条件:

(a) 结构性:其逻辑结构必须等价于完全二叉树,所以对于有n个节点的完全二叉堆,其高度h=O(logn);

(b) 堆序性:就优先级而言,堆顶以外的每个节点都不高(低)于其父节点,分别称为大(小)堆。

完全二叉堆的拓扑联接结构,完全由其规模n确定,按照层次通历的次序,每个节点都对应于唯一的编号,反之亦然,故若将所有节点组织为一个向量,则堆中各节点(编号)与向量各单元(秩)也将彼此一对应。事实上,完全二叉堆实际就是一个vector,只不过用二叉树的概念来操作,节点之间的父子关系是通过秩的计算公式来描述的

父子关系计算:设i(x)表示节点x所对应的秩,则对于完全二叉堆中的任意节点v,必然满足

(a) 若v有左孩子,则其左孩子对应的秩 i(lchild(v))=2*i(v)+1;

(b) 若v有右孩子,则其右孩子对应的秩 i(rchild(v))=2*i(v)+2;

(c) 若v有父节点,则其父节点对应的秩 i(parent(v))=[i(v)/2](上取整)-1;

四、完全二叉堆的操作

(1) 元素插入

(a) 首先调用向量的标准插入接口,将新词条接至向量的未尾,这个步骤不会导致完全二叉堆的结构性发生变化。

(b) 只要新插入的词条e不是堆顶,就有可能与其父亲违反堆序性,需要从e开始进行上滤操作进行堆序性的调整。

上滤操作:节点e与其父亲进行关键码(优先级的比较),若违背堆序性,则令二者交换位置。当然,交换之后可能导致上层祖先节点之间的堆序性异常,故需要继续往上迭代,知道不再发生堆序性异常或者到达根节点 。

性能:上滤过程的总交换次数不会超过全堆的总高度,而每次交换需要常数时间,所以上滤操作及整个插入操作的时间复杂度为O(logn)

(2) 元素删除

(a) 首先,既然待删除词条r总是位于堆顶,故可直接将其取出并备份,为防止堆的结构性将被破坏,将堆尾的节点转移到堆定。

(b) 新节点可能与其孩子违背堆序性,为了修复这一缺陷,需要从堆顶开始进行下滤操作进行调整。

下滤操作:若堆顶e不满足堆序性,可将e及其孩子中的最大者作为新的堆顶,此后可能被交换的孩子可能导致再次违背堆序性,则需要继续进行下滤迭代,直到没有堆序性异常或者到达叶节点。

性能:下滤过程的总交换次数不会超过全堆的总高度,而每次交换需要常数时间,所以下滤操作及整个删除操作的时间复杂度为O(logn)

(3) 建堆(Floyd算法)

多数情况下,输入词条集均以向量的形式给出,故除了通过个单元的秩明确对应的父子关系外,不需要做其他实质的操作。因为二叉堆本身就是一个向量,所以其结构性已经得到了满足,但是完全二叉堆的堆序性却很可能不满足,所以需要从堆的最后一个内部节点开始进行下滤操作,一直到第一个向量元素即可,这即为Floyd算法---自下而上的下滤。

性能:算法需要做n次下滤,每个节点下滤的所需要的实际正比于其高度,所有总体运行时间取决于各节点高度的总和,经过计算为O(n)。 

五、完全二叉堆的实现

首先建立了优先级队列的模板类PQ,描述了优先级队列的通用操作接口,然后基于vector和PQ的多继承得到完全二叉树类PQ_ComplHeap,其具体实现了完全二叉树的插入和删除操作,以及上滤和下滤操作。

PQ接口列表
操作 功能 对象
insert(T) 按照比较器确定的优先级次序插入词条 优先级队列
getMax() 取出优先级最高的词条 优先级队列
delMax() 删除优先级最高的词条 优先级队列
PQ_ComplHeap接口列表
操作 功能 对象
PQ_ComplHeap() 默认构造函数 完全二叉堆
PQ_ComplHeap(vector<T> &A, Rank n) 构造函数,由向量批量构造 完全二叉堆
percolateDown(Rank n, Rank i) 下滤 完全二叉堆
percolateUp(Rank i) 上滤 完全二叉堆
heapify(Rank n) Floyd建堆算法  
insert(T e) 按照比较器确定的优先次序,插入词条 完全二叉堆
getMax() 读取优先级最高的词条 完全二叉堆
delMax() 删除优先级最高的词条 完全二叉堆
     

(1) PQ.h

#pragma once

template<typename T> struct PQ   //优先级队列PQ模板类
{
	virtual void insert(T) = 0;    //按照比较器确定的优先级次序插入词条
	virtual T getMax() = 0;   //取出优先级最高的词条
	virtual T delMax() = 0;   //删除优先级最高的词条
};

(2) PQ_ComplHeap.h

#pragma once
#include"vector.h"
#include"PQ.h"

#define lt(a,b) ((a)<(b))    //比较大小
#define InHeap(n,p) (((-1)<(i))&&((i)<(n)))  //判断PQ[i]是否合法
#define Parent(i) ((i-1)>>1)    //PQ[i]的父节点(floor((i-1)/2),i无论正负)
#define LastInternal(n) Parent(n-1)   //最后一个内部节点(末节点的父亲)
#define LChild(i) (1+((i)<<1))  //PQ[i]的左孩子
#define RChild(i) ((1+(i))<<1)  //PQ[i]的右孩子
#define ParentValid(i) (0<(i))  //判断PQ[i]是否有父亲
#define LChildValid(n,i) InHeap(n,LChild(i)) //判断PQ[i]是否有一个(左)孩子
#define RChildValid(n,i) InHeap(n,RChild(i)) //判断PQ[i]是否有两个孩子
#define Bigger(PQ,i,j)  (lt(PQ[i],PQ[j])?j:i)   //取大者
#define ProperParent(PQ,n,i) \
			(RChildValid(n,i)?Bigger(PQ,Bigger(PQ,i,LChild(i)),RChild(i)):\
			(LChildValid(n,i)?Bigger(PQ,i,LChild(i)):i\
			)\
			)//父子三者中的最大者

template<typename T> class PQ_ComplHeap :public PQ<T>, public vector<T>
{
protected:
	Rank percolateDown(Rank n, Rank i);   //下滤
	Rank percolateUp(Rank i);   //上滤
	void heapify(Rank n);   //Floyd建堆算法

public:
	//构造函数
	PQ_ComplHeap() {} 
	PQ_ComplHeap(vector<T> &A, Rank n);    //批量构造
	void insert(T e);   //按照比较器确定的优先次序,插入词条
	T getMax();  //读取优先级最高的词条
	T delMax();  //删除优先级最高的词条
};

template<typename T> PQ_ComplHeap<T>::PQ_ComplHeap(vector<T> &A, Rank n)
{
	copyFrom(A, 0, n);
	heapify(n);
}

template<typename T> T PQ_ComplHeap<T>::getMax()
{
	return _elem[0];  //返回向量的首元素
}

template<typename T> void PQ_ComplHeap<T>::insert(T e)
{
	vector<T>::insert(e);  //首先将新词条接至向量末尾
	percolateUp(_size - 1);  //再对该词条实施上滤调整
}

template<typename T> Rank PQ_ComplHeap<T>::percolateUp(Rank i)
{
	while (ParentValid(i))   //只要顶点还有父亲
	{
		Rank j = Parent(i); //将i之父记作j
		if (lt(_elem[i], _elem[j])) break;    //一旦当前父子不在逆序,则上滤完成
		swap(_elem[i], _elem[j]);
		i = j;  //继续向上
	}
	return i;  //返回上滤最终抵达的位置
}

template<typename T> T PQ_ComplHeap<T>::delMax()
{
	T maxElem = _elem[0];   //缓存待删除的堆顶
	_elem[0] = _elem[--_size];  //将末尾词条放至堆顶
	percolateDown(_size, 0);    //对新堆顶进行下滤
	return maxElem;  //返回删除的原堆顶
}

template<typename T> Rank PQ_ComplHeap<T>::percolateDown(Rank n, Rank i)
{
	Rank j;  //i节点及其孩子中,最大的词条对应的秩
	while (i != (j = ProperParent(_elem, n, i)))   //只要最大者不为i本身
	{
		swap(_elem[i], _elem[j]);   //最大者变为新父亲
		i = j;   //继续向下
	}
	return i;  //返回下滤最终到达的位置
}

template<typename T> void PQ_ComplHeap<T>::heapify(Rank n)
{
	for (int i = LastInternal(n); InHeap(n, i); i--)   //从堆末词条的父亲开始下滤
		percolateDown(n, i);   //下滤各内节点
}

猜你喜欢

转载自blog.csdn.net/qq_18108083/article/details/85203134