priority_queue又叫优先队列,其实应该算是一个容器适配器,存储结构默认设置为vector,特点是每次使用pop()都会弹出最大优先级的那个元素,原因就是这个容器的排序准则是维持一个大根堆(1.根节点要大于等于左右节点,但是左右节点没有要求谁大谁小。2.元素的逻辑模型应该为一颗完全二叉树),在源码中可以窥见一二
数据元素类型为T,默认使用vector存储元素,排序准则默认为less
priority_queue最重要的两个函数pop和push定义如下:
void push(const value_type& x) { __STL_TRY { /*压入一个新值放入最后*/ c.push_back(x); /*堆化处理*/ push_heap(c.begin(), c.end(), comp); } __STL_UNWIND(c.clear()); } void pop() { __STL_TRY { /*将根部元素和最后一片叶子交换(此时最后一个元素为根值)然后把除去最后一个元素的剩余元素做重新堆化处理*/ /*注意这里最大的元素还在最后一个位置*/ pop_heap(c.begin(), c.end(), comp); /*真正弹出元素*/ c.pop_back(); } __STL_UNWIND(c.clear()); }
当插入元素的时候,会先调用底部容器的push_back方法往最后一个位置插入元素,然后调用push_heap重新堆化
堆的性质:当节点有两个子节点时,假设当前下边为i(i不等于0),则左孩子下标为2i,右边孩子下标为2i+1;当一个点不是顶点时,其父节点下标为n/2。
但是stl实作里顶点下标是0,使用情况稍微复杂了点,知道什么意思就行。
template <class RandomAccessIterator, class Distance, class T> void __push_heap(RandomAccessIterator first, Distance holeIndex, Distance topIndex, T value) { /*找出待检查点的父节点位置*/ /*待检查点开始从下往上检查*/ Distance parent = (holeIndex - 1) / 2; /*未到达顶端,且父节点小于插入点*/ while (holeIndex > topIndex && *(first + parent) < value) { /*父节点移到子节点(值交换)*/ *(first + holeIndex) = *(first + parent); /*与父节点交换位置*/ holeIndex = parent; /*重新计算父节点位置*/ parent = (holeIndex - 1) / 2; } /*设置目标值,完成插入操作*/ *(first + holeIndex) = value; } template <class RandomAccessIterator, class Distance, class T> inline void __push_heap_aux(RandomAccessIterator first, RandomAccessIterator last, Distance*, T*) { __push_heap(first, Distance((last - first) - 1), Distance(0), T(*(last - 1))); } template <class RandomAccessIterator> inline void push_heap(RandomAccessIterator first, RandomAccessIterator last) { __push_heap_aux(first, last, distance_type(first), value_type(first)); }
删除元素:优先队列每次删除的都是优先级最大(或最小)的那个节点,stl的priority_queue默认是大根堆规则形成的vector,下边不再多说;
pop的源码如下,这里选用的是compare为默认值的情况(less)
/* first 首迭代器 holeIndex 目标索引 len 待检查长度 value 原尾部元素 */ template <class RandomAccessIterator, class Distance, class T> void __adjust_heap(RandomAccessIterator first, Distance holeIndex, Distance len, T value) { /*先记下开始检查的节点下标*/ /*pop操作的话就是根部下标,因为要从顶点往下检查是否有子节点大于父节点*/ Distance topIndex = holeIndex; /* 1.更换顶端节点后重新堆化 */ /*先计算根节点的大儿子节点*/ Distance secondChild = 2 * holeIndex + 2; //(1) /*下标没超过堆长度*/ while (secondChild < len) { /*左右子节点去比较,选出大的那一个,secondChild就是大的那一个的下标*/ if (*(first + secondChild) < *(first + (secondChild - 1))) secondChild--; /*父节点值与大儿子值交换*/ *(first + holeIndex) = *(first + secondChild); /*下一层检查,父下标变大儿子下标*/ holeIndex = secondChild; /*计算新的大儿子下标 为什么这里和(1)的计算方式不一样呢,上边是+2,这里是+1 其实就是因为要兼容上边(1)的时候holeIndex是0的情况,使得每次secondChild默认值是右孩子 */ secondChild = 2 * (secondChild + 1); //(2) } /*没有右子节点,只有左子节点*/ if (secondChild == len) { *(first + holeIndex) = *(first + (secondChild - 1)); holeIndex = secondChild - 1; } /* 2.将原最后一个元素放入合适位置 */ __push_heap(first, holeIndex, topIndex, value); } template <class RandomAccessIterator, class T, class Distance> inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last, RandomAccessIterator result, T value, Distance*) { /*最后一个元素设置为根部的值*/ *result = *first; /*这里的last已经是指向最后一个节点了,不是原来的最后一个节点的下一个节点*/ /*所以Distance(last-first)是排除最后一个元素后的剩余系列长度*/ __adjust_heap(first, Distance(0), Distance(last - first), value); } template <class RandomAccessIterator, class T> inline void __pop_heap_aux(RandomAccessIterator first, RandomAccessIterator last, T*) { __pop_heap(first, last - 1, last - 1, T(*(last - 1)), distance_type(first)); } template <class RandomAccessIterator> inline void pop_heap(RandomAccessIterator first, RandomAccessIterator last) { __pop_heap_aux(first, last, value_type(first)); }
要想深入理解还是得亲手调试几遍,迭代器改成指针,长度什么的改成int,用int数组调试就够了,复制到windows下用vs调(注意:进行pop操作前元素序列必须是有效大根堆)
关于优先队列还有两个函数可以谈,分别是它的sort排序函数和将一个序列堆化的函数made_heap
sort_heap:前边讲过每次pop会把优先级最高的放置于尾部,调用while循环pop即可形成一个递增序列
template <class RandomAccessIterator> void sort_heap(RandomAccessIterator first, RandomAccessIterator last) { /*从尾部调用pop_heap直到根部*/ while (last - first > 1) pop_heap(first, last--); }
made_heap:将一个序列变成大根堆
template <class RandomAccessIterator, class T, class Distance> void __make_heap(RandomAccessIterator first, RandomAccessIterator last, T*, Distance*) { /*一个或者0个元素不用排*/ if (last - first < 2) return; /*长度*/ Distance len = last - first; /*找第一个需要重排的节点下标*/ /*parent这个命名有些歧义,还是较holeIndex较好*/ Distance parent = (len - 2)/2; while (true) { /*以parent开始检查,直到顶点*/ /*以parent这个点为每个小堆的顶点去做堆化处理*/ __adjust_heap(first, parent, len, T(*(first + parent))); if (parent == 0) return; parent--; } }
参考:
stl源码剖析4.8priority queue
数据结构于算法第二版 陈卫卫,王庆瑞第五章堆排序
大话数据结构第九章堆排序
关于算法还是一句话,调试是关键,硬看不推荐,实际上理解了它的运行模式也差不多了,调一下加深印象,脑袋要炸了,过两天可能就忘了,人之常情,作此记录以待需要可以快速拾起罢了。