优先级队列
优先级队列是一种循优先级访问的不失高效的轻量级数据结构,优先级队列只需要维持偏序即可满足设计目标。优先级队列的实现依托于向量的“肉”和二叉树的“灵”,完全二叉堆与左式堆是优先级队列实现的两种方式。
接口 | 功能 |
---|---|
insert( T ) | 按照优先级次序插入词条 |
getMax( ) | 取出优先级最高的词条 |
delMax( ) | 删除优先级最高的词条 |
1 完全二叉堆
堆序性:堆顶之外的每个节点都不高于其父节点。以下是完全二叉堆的实现程序。
1.1 完全二叉堆的实现
#include "../dsa_vector_200622/dsa_vector.h" //借助多重继承机制,基于向量
#include "pq_macro.h"
#include "pq.h" //按照优先级队列ADT实现的
template <typename T> class PQ_ComplHeap : public PQ<T>, public Vector<T> { //完全二叉堆
// /*DSA*/friend class UniPrint; //演示输出使用,否则不必设置友类
protected:
Rank percolateDown ( Rank n, Rank i ); //下滤
Rank percolateUp ( Rank i ); //上滤
void heapify ( Rank n ); //Floyd建堆算法
public:
PQ_ComplHeap() { } //默认构造
PQ_ComplHeap ( T* A, Rank n ) { this->copyFrom ( A, 0, n ); heapify ( n ); } //批量构造
void insert ( T ); //按照比较器确定的优先级次序,插入词条
T getMax(); //读取优先级最高的词条
T delMax(); //删除优先级最高的词条
}; //PQ_ComplHeap
//getMax()
template <typename T> T PQ_ComplHeap<T>::getMax() { return this->_elem[0]; }
//insert
template <typename T> void PQ_ComplHeap<T>::insert ( T e ) { //将词条插入完全二叉堆中
Vector<T>::insert ( e ); //首先将新词条接至向量末尾
percolateUp ( this->_size - 1 ); //再对该词条实施上滤调整
}
void swap( int &a, int &b){
int tmp = a;
a = b;
b = tmp;
}
//上滤
//对向量中的第i个词条实施上滤操作,i < _size
template <typename T> Rank PQ_ComplHeap<T>::percolateUp ( Rank i ) {
while ( ParentValid ( i ) ) { //只要i有父亲(尚未抵达堆顶),则
Rank j = Parent ( i ); //将i之父记作j
if ( this->_elem[i] <= this->_elem[j] ) break; //一旦当前父子不再逆序,上滤旋即完成
swap ( this->_elem[i], this->_elem[j] ); i = j; //否则,父子交换位置,并继续考查上一层
} //while
return i; //返回上滤最终抵达的位置
}
//delMax()
template <typename T> T PQ_ComplHeap<T>::delMax() { //删除非空完全二叉堆中优先级最高的词条
T maxElem = this->_elem[0]; this->_elem[0] = this->_elem[ --this->_size ]; //摘除堆顶(首词条),代之以末词条
percolateDown ( this->_size, 0 ); //对新堆顶实施下滤
return maxElem; //返回此前备份的最大词条
}
//对向量前n个词条中的第i个实施下滤,i < n
template <typename T> Rank PQ_ComplHeap<T>::percolateDown ( Rank n, Rank i ) {
Rank j; //i及其(至多两个)孩子中,堪为父者
while ( i != ( j = ProperParent ( this->_elem, n, i ) ) ) //只要i非j,则
{ swap ( this->_elem[i], this->_elem[j] ); i = j; } //二者换位,并继续考查下降后的i
return i; //返回下滤抵达的位置(亦i亦j)
}
//Floyd建堆
template <typename T> void PQ_ComplHeap<T>::heapify ( Rank n ) { //Floyd建堆算法,O(n)时间
for ( int i = LastInternal ( n ); InHeap ( n, i ); i-- ) //自底而上,依次
/*DSA*/
percolateDown ( n, i ); //下滤各内部节点
// /*DSA*/for ( int k = 0; k < n; k++ ) {
// /*DSA*/ int kk = k; while ( i < kk ) kk = (kk - 1) / 2;
// /*DSA*/ i == kk ? print(_elem[k]) : print(" " );
// /*DSA*/}; printf("\n");
// /*DSA*/}
}
template <typename T> void Vector<T>::heapSort ( Rank lo, Rank hi ) { //0 <= lo < hi <= size
PQ_ComplHeap<T> H ( _elem + lo, hi - lo ); //将待排序区间建成一个完全二叉堆,O(n)
while ( ! H.empty() ) //反复地摘除最大元并归入已排序的后缀,直至堆空
_elem[--hi] = H.delMax(); //等效于堆顶与末元素对换后下滤
}
其包含的头文件如下"…/dsa_vector_200622/dsa_vector.h"
//"pq.h"
#pragma once
template <typename T> struct PQ { //优先级队列PQ模板类
virtual void insert ( T ) = 0; //按照比较器确定的优先级次序插入词条
virtual T getMax() = 0; //取出优先级最高的词条
virtual T delMax() = 0; //删除优先级最高的词条
};
//"pq_macro.h"
#pragma once
#define InHeap(n, i) ( ( ( -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) ( 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 \
) \
) //相等时父节点优先,如此可避免不必要的交换
1.2 完全二叉堆的测试
以下是对于完全二叉堆的测试,依次测试了建堆函数、getMax、delMax与堆排序。结果见注释,可知见测试成功。
#include "pq_complheap.h"
#include <iostream>
using namespace std;
int main(){
int A[] = { 4, 2, 5, 1, 3};
//测试建堆函数
PQ_ComplHeap<int> a (A, 5);
//测试getMax
cout << a.getMax() << endl;//5
//测试insert
a.insert( 6 );
//测试delMax
cout << a.delMax() << endl;//6
cout << a.getMax() << endl;//5
//测试堆排序
Vector<int> vec(A, 5);
vec.heapSort( 0, 5);
for( int i= 0; i<5; ++i )
cout << vec[i] << " ";//1 2 3 4 5
}
2 左式堆
-
由于完全二叉堆不能高效地完成堆的合并,所以提出左式堆的实现方法。
-
左式堆的基本思路是:在保持堆序性的前提下附加新的条件,使得在堆的合并过程中,只需调整很少量的节点。
-
空节点路径长度: npl(null path length)。npl(x)既等于x到外部节点的最近距离,同时也等于以x为根的最大满子树的高度。
-
左式堆:任意节点x,满足npl( lc(x) ) >= npl( rc(x) )
-
最右侧通路:rPath( x )。从x沿着右侧分支一直前行到空节点,每个点的npl值等于其最右侧通路的长度。r为根节点,rPath( r )终点是深度最小的外部节点。
2.1 左式堆的实现
左式堆的核心是merge的实现方法,insert和delMax的实现都依赖于merge。
#pragma once
#include "pq.h" //引入优先级队列ADT
#include "../dsa_bintree_20200720/bintree.h" //引入二叉树节点模板类
template <typename T>
class PQ_LeftHeap : public PQ<T>, public BinTree<T> { //基于二叉树,以左式堆形式实现的PQ
/*DSA*/friend class UniPrint; //演示输出使用,否则不必设置友类
public:
PQ_LeftHeap() { } //默认构造
PQ_LeftHeap ( T* E, int n ) //批量构造:可改进为Floyd建堆算法
{ for ( int i = 0; i < n; i++ ) insert ( E[i] ); }
void insert ( T ); //按照比较器确定的优先级次序插入元素
T getMax(); //取出优先级最高的元素
T delMax(); //删除优先级最高的元素
}; //PQ_LeftHeap
//getMax
template <typename T> T PQ_LeftHeap<T>::getMax() //获取非空左式堆中优先级最高的词条
{ return this->_root->data; } //按照此处约定,堆顶即优先级最高的词条
//swap
void swap( BinNodePosi(int) &a,BinNodePosi(int) &b){
auto tmp = a;
a = b;
b = tmp;
}
//merge
template <typename T> //根据相对优先级确定适宜的方式,合并以a和b为根节点的两个左式堆
static BinNodePosi(T) merge ( BinNodePosi(T) a, BinNodePosi(T) b ) {
if ( ! a ) return b; //退化情况
if ( ! b ) return a; //退化情况
if ( a->data <= b->data ) swap ( a, b ); //一般情况:首先确保b不大
a->rc = merge ( a->rc, b ); //将a的右子堆,与b合并
a->rc->parent = a; //并更新父子关系
if ( !a->lc || a->lc->npl < a->rc->npl ) //若有必要
swap ( a->lc, a->rc ); //交换a的左、右子堆,以确保右子堆的npl不大
a->npl = a->rc ? a->rc->npl + 1 : 1; //更新a的npl
return a; //返回合并后的堆顶
} //本算法只实现结构上的合并,堆的规模须由上层调用者负责更新
//delMax
template <typename T> T PQ_LeftHeap<T>::delMax() { //基于合并操作的词条删除算法(当前队列非空)
BinNodePosi(T) lHeap = this->_root->lc; //左子堆
BinNodePosi(T) rHeap = this->_root->rc; //右子堆
T e = this->_root->data; delete this->_root; this->_size--; //删除根节点
this->_root = merge ( lHeap, rHeap ); //原左右子堆合并
// if ( _root ) _root->parent = NULL; //若堆非空,还需相应设置父子链接
return e; //返回原根节点的数据项
}
//insert
template <typename T> void PQ_LeftHeap<T>::insert ( T e ) { //基于合并操作的词条插入算法
BinNodePosi(T) v = new BinNode<T> ( e ); //为e创建一个二叉树节点
this->_root = merge ( this->_root, v ); //通过合并完成新节点的插入
// _root->parent = NULL; //既然此时堆非空,还需相应设置父子链接
this->_size++; //更新规模
}
2.2 左式堆的测试
左式堆的测试程序如下所示,测试项目包括getMax,delMax,insert与merge,结果如注释所示,可见测试成功。
#include "pq_leftheap.h"
#include <iostream>
using namespace std;
int main(){
int A[] = { 2, 5, 1, 7, 18};
PQ_LeftHeap<int> lh1( A, 5);
//getMax()
cout << lh1.getMax() << endl;//18
//delMax
cout << lh1.delMax() << endl;//18
//insert
lh1.insert( 11 );
cout << lh1.getMax() << endl;//11
//merge
int B[] = { 12, 5, 21, 6, 9};
PQ_LeftHeap<int> lh2(B, 5);
//PQ_LeftHeap<int> lh( merge( lh1.root() , lh2.root()));
cout << merge( lh1.root() , lh2.root())->data << endl;//21
cout << lh2.delMax() << endl;//21
cout << lh2.delMax() << endl;//12
cout << lh2.delMax() << endl;//11,可见lh2合并入lh2中了
cout << lh2.delMax() << endl;//9
cout << lh2.delMax() << endl;//7
}
3 优先级队列总结
优先级队列是一个轻量级的实现“循优先级”访问的数据结构,它包括完全二叉堆与左式堆两种实现形式。其中,完全二叉堆可以高效地完成各项操作,但是merge操作仍然有待改进;所以,为提高merge的效率,左式堆被提了出来,基于merge的insert和delMax也被设计出来。