一、优先级队列
- 功能接口
template <typename T>struct PQ{//Priority Queue
virtual void insert(T) = 0;//按照优先级次序插入词条
virtual T getMax() = 0;//取出优先级最高的词条
virtual T delMax() = 0;//删除优先级最高的词条
};//与其说PQ是数据结构,不如说是ADT;其不同的实现方式,效率及适应场合也各不相同
- 完全二叉堆Complete Binary Heap(效率兼顾成本)
- 若要查找极值元,则不必维护所有元素之间的全序关系,偏序足以。
- 逻辑上,等同于完全二叉树;物理上,直接借助向量实现。
- 逻辑节点与物理元素依层次遍历次序彼此对应,秩为i的节点
- 父亲节点:(i - 1) / 2
- 左孩子:1 + 2 * i
- 右孩子:(1 + i ) * 2
#define Parent(i) ((i - 1) >> 1
#define LChild(i) (1 + ((i) << 1))//奇数
#define RChild(i) ((1 + (i)) << 1)//偶数
- 完全二叉堆的实现
//PQ_ComplHeap = PQ + Vector
//以公开的模式,同时继承了PriorityQueue和Vector的特性,即在物理上拥有了一个_elem[]的可扩充数组
template <typename T> class PO_ComplHeap:public PQ<T>, public Vector<T>{
protected: Rank percloateDown(Rank n, Rank i);//下滤
Rank percolateUp(Rank i);//上滤
void heapify(Rank n);//Floyd建堆算法
public: PQ_ComplHeap(T* A, Rank n)//批量构造
{copyFrom(A, 0, n); heapify(n);}
void insert(T);//按照比较器确定的优先级次序,插入词条
T getMax(){return _elem[0];}//读取优先级最高的词条
T delMax();//删除优先级最高的词条
};
-
堆序性
- 任何一个节点在数值上都不会超过它的父亲
- 故根节点为最大元
-
插入与上滤
- 为插入词条e,只需将e作为末元素接入向量。
- 结构性自然保持。
- 若堆序新也亦未破坏,则完成。
- 上滤
- 若堆序性被破坏,只能是e与其父节点违反堆序性, 将e与其父节点换位。还要判断e与其新的父亲的堆序性,不断重复,直到根节点。
- 实现
- 优化:实现中采用swap函数,则要赋值三次,最坏情况下累计要赋值3logn。针对该情况进行优化,首先将新插入的词条备份,每次如有必要交换,只是下移它的父亲节点,直到能够确定e已经无需上溢时,再将此前备份的词条纳入最终的位置。实现将赋值操作的次数从3logn减少至logn+2.
- 为插入词条e,只需将e作为末元素接入向量。
template<typename T> void PQ_ComplHeap<T>::insert(T e)//
{Vector<T>::insert(e); percolateUp(_size - 1);}
template<typename T>//对第i个词条实施上滤,i < _size
Rank PQ_ComplHeap<T>::percolateUp(Rank i){
while(ParentValid(i)){//只要i有父亲(尚未抵达堆顶),则
Rank j = Parent(i);//将i之父记作j
if(lt(_elem[i], _elem[j])) break;//一旦父子不再逆序,上滤完成
swap(_elem[i], elem[j]); i = j;//否则,交换父子位置,并上升一层
}//while
return j;//返回上滤最终抵达的位置
}
- 删除与下滤
- 最大元素始终在堆顶,故删除之,只需要O(1)
- 摘除向量的首元素,代之以末元素e
- 结构性保持
- 若堆序性依然保持则完成
- 下滤
- 若堆序性被破坏,只能是e与孩子们违背了堆序性。将e与孩子中的最大者换位,若堆序性因此恢复,则完成。不断重复直到叶子节点。
- 效率O(logn)
template <typename T> TPQ_ComplHeap<T>::delMax(){//删除
T maxElem = _elem[0]; _elem[0] = _elem[--_size];//摘除堆顶,代之以末词条
percolateDown(_size, 0);//对新堆顶实施下滤
return maxElem;//返回此前备份的最大词条
}
template <typename T>//对前n个词条中的第i个实施下滤,i<n
Rank PQ_ComplHeap<T>::percolateDown(Rank n, Rank i){
Rank j;//i及其(至多两个)孩子中,堪为父者
while(i != (j = ProperParent(_elem, n, i)))//只要i非j,则
//该循环退出有两种可能,第一种是i大于它的任何一个孩子,第二种是i持续下滤到最底层,并成为一片叶子。
{swap(_elem[i], _elem[j]); i = j;}//换位,并继续考察i
return i;//返回下滤抵达的位置
}
二、批量建堆Heapification
- 定义:将任意n个元素建造成完全二叉堆
- 自上而下的上滤
- 最坏情况下,每个节点都要上滤至根,所需的成本线性正比于其深度
- 即便只考虑底层,n / 2个叶节点,深度均为O(logn),累计耗时O(nlogn)
- 这样长的时间,足以全排序,可以优化。
PQ_ComplHeap(T* A, Rank n){copyFrom(A, 0, n); heapify(n);}
//以任意指定的规模为n的数组为蓝本,将其中的元素组成一个完全二叉堆。
//先调用向量的copyFrom接口,将这个数组复制到内部,实质的操作则是调用heapify算法将元素调整为堆。
//蛮力算法
template<typename T> void PQ_ComplHeap<T>::heapify(Rank n){
//自上而下,自左向右
for(int i = 1; i < n ; i++)//按照层次遍历次序逐一
percolateUp(i);//对每一个元素做上滤处理
}
- 自下而上的下滤
- 任意给定堆H0和H1,以及节点p,为得到该三者组成的堆,只需将r0和r1当作p的孩子,对p下滤
- 实现:Floyd算法
- 效率
- 每个内部节点所需的调整时间,正比于其高度而非深度。在渐进意义上每个节点的高度和为O(n)
- 蛮力算法的效率是对其深度求和。二者之所以有如此大的差异,是因为在完全二叉树中越是靠近底层,节点越多。
template<typename T>
void PQ_ComplHeap<T>::heapify(Rank n){
for(int i = LastInternal(n); i >= 0; i--)//从最左边叶子节点的父节点处开始,合并子堆。自下而上,自右向左
percolateDown(n, i);//依次下滤各内部节点
}//可理解为子堆的逐层合并
三、堆排序
- 流程
- 初始化:heapify(), O(n)建堆
- 迭代:delMax(), O(logn),取出堆顶并调整复原
- 不变性:H<=S,即待排序部分不超过已排序部分
- 就地实现
- 已排序部分位于向量的后端,而与之互补的前缀恰好构成一个完全二叉堆。
- 在大顶堆中,堆中的最大元始终是0号元素,而需要与之对换的x则必然是相对于已排序元素而言秩为-1的那个元素。通过将最大元与x反复的交换、下滤,实现堆变空。
template <typename T>//对向量区间[lo, hi)做就地堆排序
void Vector<T>::heapSort(Rank lo, Rank hi){
PQ_ComplHeap<T> H(_elem + lo, hi - lo);//待排区间建堆,O(n)
while(!H.empty())//反复地摘除最大元并归入已排序的后缀,直至堆空
_elem[--hi] = H.delMax();//等效于堆顶与末元素对换后下滤
}