优先级队列(Priority Queue)
优先级队列是一种按照优先级访问的数据结构,在PQ中每个元素都具有自己的优先级,每次都是访问优先级最大的元素,借助完全二叉树可以实现优先级队列。完全二叉树指的是每一层都是满二叉树,并且最底层非空的元素都处于左边。
若完全二叉树根节点的秩为0,后续元素的秩按照层序递增,那么在完全二叉树中有以下对应关系:
(1)父节点的秩等于子节点的秩减1后整除2;
(2)左孩子的子等于父节点的秩乘以2+1;
(3)右孩子的秩等于父节点的秩乘以2+2;
即可定义如下关系:
#define Parent(i) (i-1)>>1
#define LChild(i) 1+i<<1
#define RChild(i) (1+i)<<1
基于此可以以向量的形式组织完全二叉树。完全二叉堆要求满足堆序性,即在对应组织的完全二叉树中任意父节点的优先级数均不低于子节点的优先级数,由此可得完全二叉堆中优先级最大的元素就是根节点,因此访问最大元只需要O(1)的时间复杂度。
插入算法
插入优先级队列时,只需要将待插入元素插到对应完全二叉树的最后,但是这样堆序性可能被破坏,当插入元素e的优先级大于其父节点的优先级时,将e与父节点换位置即可,但是这一情况可能上溢到根节点。
void PQ_ComplHeap<T>::Insert(const T& e)
{
Vector<T>::insert(e);
percolateUp(_size-1);
}
Rank PQ_ComplHeap<T>::percolateUp(Rank i)
{
Rank _hot=i;
while(Parent(i)>0)
{
Ranl pRank=Parent(i);//记录当前节点的父节点
if(_elem(pRank)<_elem(i))//若违背堆序性
{
swap(_elem(pRank,_elem(i));
_hot=pRank;
i=pRank;
}
else return _hot;
}
}
由于最多上滤到根节点,所以插入算法的时间复杂度为O(logn);
删除算法
优先级最高的元素已经处于根节点,因此可直接将根节点删除,为了维护结构性和堆序性,应在删除节点的左右后代中挑选出优先级较高的一个,与根节点互换位置,但是如此一来可能导致低一层的节点发生下溢,如此可递推处理,直至满足堆序性,或者e已经成了叶节点。
T PQ_ComplHeap<T>::DeleteMax()
{
T maxElem=_elem[0];
_elem[0]=_elem[_size-1];
while(true)
{
Rank lc=LChild(i);
Rank rc=RChild(i);
if(rc>_size-2||(_elem(lc)<=_elem(i) && _elem(Rc)<=_elem(i)))//递归基:下滤到叶节点或已经满足堆序性
{
return i;
}
if(_elem[lc]>_elem[rc])//左子树更大,替换左子树
{
swap(_elem[i],_elem[lc]);
i=lc;
}
else//右子树更大,替换右子树
{
swap(_elem[i],_elem[rc]);
i=rc;
}
}
return maxElem;
}
批量建堆
蛮力建堆
在空的完全二叉堆中逐个插入元素,然后上滤节点。
template<typename T>
void PQ_ComplHeap<T>::heapify(T* A,Rank n)
{
copy_From(A,0,n);
for(int i=1;i<n;++i)//从第二层开始,每个节点往上比,逐层上滤,越往下上滤次数越多
{
while(parent(i)>0)
{
Rank pRank=Parent(i);
if(_elem[i]<_elem[pRank])
{
break
}
else
{
swap(_elem[i],_elem[pRank];
i=pRank;
}
}
}
}
由于每个节点最坏的情况下都需要上滤至根节点,因此最坏情况下时间复杂度为∑Depth(i)~nlogn。
Floyd建堆算法
从最后一个节点的父亲开始,自上而下的进行下滤,直至根节点结束。
template<typename T>
void PQ_ComplHeap<T>::heapify(T* A,Rank n)
{
copy_From(A,0,n);
Rank lastNode=n-1;
for(int i=0;i=Parent(n-1);--i)//从最后一个节点的父节点开始,进行下滤,迭代到根节点。
{
Rank lc=LChild(i);
Rank rc=RChild(i);
while(true)
{
Rank lc=LChild(i);
Rank rc=RChild(i);
if(rc>_size-1||_elem(lc)<=_elem(i) && _elem(Rc)<=_elem(i))
{
break;
}
if(_elem[lc]>_elem[rc])
{
swap(_elem[i],_elem[lc]);
i=lc;
}
else
{
swap(_elem[i],_elem[rc]);
i=rc;
}
}
}
}
复杂度分析:
考虑满二叉树,节点为n的满二叉树最后一层有(n+1)/2个节点,但下滤高度为0,倒数第二层节点数减半,但是下滤高度加1...即最底层的节点数量最多,但是下滤的高度也最少,越往上节点数量越少,但是下滤的高度也越高,类似于金字塔效应。最后可以算的,该建堆方法的复杂度为O(n)。
堆排序
利用优先二叉堆可对选择排序进行改进,之前以数组的形式组织的元素,如今可以以优先二叉堆的方式组织,具体思路为:
(1)首先将待排序元素建堆(O(n));
(2)取出堆顶元素,与队列中最后一个元素交换位置,调用优先二叉堆的下滤算法;
(3)取出新的堆顶元素,插入到已排序部分的开始;
(4)重复(2)(3),直至已排好序。
下滤过程的实现:n为堆中的总数,从编号为i的节点开始下滤
//规模为n,对第一个节点下滤,迭代版本
void adjust(int* p, int n, int i)
{
while (true)
{
int lc = 2 * i + 1;
int rc = 2 * i + 2;
if (rc > n || (*(p + lc) <= *(p + i) && *(p + rc) <= *(p + i)))//递归基:左右孩子都不存在,或者存在但是已经满足堆序性
{
return;
}
else
{
if (rc == n)//左孩子存在,右孩子不存在,此时应单独考虑
{
if (*(p + lc) > *(p + i))
{
swap(*(p + lc), *(p + i));
break;
}
}
if (*(p + lc) >= *(p + rc))//左孩子更大,向左下滤
{
swap(*(p + lc), *(p + i));
i = lc;
}
else
{
swap(*(p + rc), *(p + i));//右孩子更大,向右下滤
i = rc;
}
}
}
}
//递归实现,用变量记录应该深入的节点
void adjust(int* p, int n, int i)
{
int lc = 2 * i + 1;
int rc = 2 * i + 2;
int parent = i;
if (lc<n && p[lc]>p[parent])
{
parent = lc;
}
if (rc<n && p[rc]>p[parent])
{
parent = rc;
}
if (parent!=i)
{
swap(*(p+i), *(p+parent));
adjust(p, n, parent);
}
return ;
}
void HeapSort(int* array, int lo, int hi)
{
int* p = array + lo;
for (int i = (hi - lo - 1) / 2; i >= 0; i--)//批量建堆,自下而上下滤
{
adjust(p, hi - lo+1, i);
}
for (int j = 0; j < hi - lo; ++j)//交换堆顶和有序部分的首个元素,再下滤头结点
{
swap(*p, *(p + hi - j));
adjust(p, hi - lo - j, 0);
}
}
int main()
{
int a[100] = { 0};
for (int i = 0; i < 100; ++i)
{
a[i]=rand()%100;
}
HeapSort(a, 0, 99);
for (int i = 0; i < 100; ++i)
{
cout << a[i] << " ";
}
system("pause");
return 0;
}