本文参考的源码版本:gcc version 8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project)。

priority_queue 本质是容器适配器,它对内部容器的元素有自己的管理方式,而 priority_queue 实际维护的是一个二叉堆。STL中 priority_queue 的操作是基于完全二叉树,使用随机访问迭代器访问元素,二叉堆在创建时按照层序遍历的顺序将数据放入容器中,因此创建 priority_queue 时使用的容器需要具有随机访问的特性。

priority_queue 是一个类模板,有三个模板参数,第一个数据的类型,第二个是容器的类型,默认为 vector ,第三个是用于比较操作的函数对象,默认是 less ,即小于比较。

template<typename _Tp, typename _Sequence = vector<_Tp>,typename _Compare  = less<typename _Sequence::value_type> >class priority_queue {};

完全二叉树

如果对满二叉树的结点进行编号, 约定编号从根结点起, 自上而下, 自左而右。则深度为 k 的, 有 n 个结点的二叉树, 当且仅当其每一个结点都与深度为 k 的满二叉树中编号从 1 至 n 的结点一一对应时, 称之为完全二叉树,换言之如果只删除了满二叉树最底层最右边的连续若干结点形成的树称为完全二叉树。满二叉树是完全二叉树的特列。完全二叉树具有以下特性:

  • 具有 n 个节点的完全二叉树的深度为:

k=[log2n]floor+1k = [log_2n]_{floor} + 1 k=[log2​n]floor​+1

  • 第 iii 个结点的编号范围为: 1≤i≤n1 ≤ i ≤ n1≤i≤n ;
  • 如果 i=1i = 1i=1,iii 为根结点,无双亲;如果 i>1i > 1i>1 ,结点iii 的双亲结点为:

[i/2]floor[i/2]_{floor}[i/2]floor​

  • 如果2i<=n2i<=n2i<=n ,结点i的左孩子为结点 2i2i2i;否则无左孩子

  • 如果 2i+1<=n2i+1<=n2i+1<=n,结点i的右孩子为结点 2i+12i+12i+1,否则无右孩子

  • 结点 iii 所在的层次为:

    ki=[log2i]floor+1k_i = [log_2i]_{floor} + 1 ki​=[log2​i]floor​+1

  • 如果 i>1i > 1i>1, iii 为奇数时,结点 iii 为右子结点; iii 为偶数时,结点 iii 为左子结点

STL的 priority_queue 在实现的时候保证了堆顶的元素始终位于容器的第一个位置,相当于二叉堆的完全二叉树的位置是从 0 开始计数,与完全二叉树的计数有细微差别。

堆有序化( reheapifying ) 中的上浮和下沉

当在二叉堆的最后一个位置插入新的元素时,新加入的元素可能会破坏堆的有序性,此时需要对新加结点与其父结点进行比较,如果大于父结点的值,那么就需要交换新加结点和父结点,如此重复比较,直到不再比父结点大时终止。这个过程就是堆有序化过程中的上浮操作。

当在移除二叉堆的堆顶元素时,被移除的元素破坏了堆的有序性,此时需要对堆顶的两个子结点中选择较大的值作为新的堆顶,而被选择的子结点则不能再作为子结点,则需继续比较其两个子结点,如此重复比较,直到到达堆底,或者两个子结点的值都比根结点小。这个过程就是堆有序化过程中的下沉操作。

#include <vector>
#include <random>
#include <chrono>using namespace std;class PriorityQueue {public:void Push(int value) {v_.emplace_back(value);push_hole(v_.size() - 1,value);}void Pop() {int back = v_.back();int size = v_.size();int hole = 0;int right_child = 2 * (hole + 1); // 计算右孩子结点的位置while (right_child < size) {// 左孩子小于右孩子if (v_[right_child - 1] < v_[right_child]) {v_[hole] = v_[right_child];hole = right_child;} else { // 左孩子大于等于右孩子v_[hole] = v_[right_child - 1];hole = right_child - 1;}right_child = 2 * (hole + 1);}push_hole(hole, back);v_.pop_back(); // 最后才从容器中删除元素}int Top() const {return v_[0];};;int Empty() const {return v_.empty();}friend ostream &operator<<(ostream &os, const PriorityQueue &rhs) {for (const auto &v: rhs.v_) {os << v << " ";}os << endl;return os;}private:void push_hole(int hole,  int value) {int parent = (hole - 1) / 2; // 根据完全二叉树的特性计算父结点的位置// 父结点比新加的值小,交换父结点和新加入的值while (parent >= 0 && v_[parent] < value) {swap(v_[parent], v_[hole]);hole = parent;parent = (hole - 1) / 2;}v_[hole] = value;}private:vector<int> v_;
};int main() {PriorityQueue pq;default_random_engine  e(chrono::system_clock::now().time_since_epoch().count());uniform_int_distribution<int> d(1,1000);for (int i = 0; i < 100; i++) {pq.Push(d(e));}
//    vector<int> v{11,10,3,20,15,15,1,17,9,2,0,
//                  12,25,11,12,30,0,0,0,60,15,
//                  22,63,77,60,1,1,2,6,7,4,2,8};
//    for (int val : v) {//        pq.Push(val);
//    }cout << "Container: " << pq;cout << "Top: ";while (!pq.Empty()) {int top = pq.Top();pq.Pop();cout << top << " ";}return 0;
}

priority_queue 的 push 操作

priority_queuepush 操作本质上就是二叉堆有序化的上浮,在真正的 push 前会先在容器的最后一个位置挖一个洞,以作为后续重排容器元素的中转位置。push 操作步骤如下:

  1. 先将要添加的数据添加到容器最后一个位置,相当于挖个洞;
  2. 然后对容器进行重排操作,重排操作的过程大致如下:
    • 初始时,洞的位置就是容器最后一个位置;
    • 如果容器本身是空的,那么在洞的位置添加新的元素后,该元素就堆顶的元素;
    • 如果容器本身不是空的,那么就通过洞的位置找到其父结点的位置,然后使用父结点的值与新加入的值进行比较,如果父结点的元素值小于新加入的值,那么就将父结点的值移到挖的洞中,这样中间位置就形成了一个洞,此时更新洞的位置。然后重复操作,直至洞的位置不再更新。
  3. 将新加入的值添加到洞中,完成一次 push 操作。

示例:

下面是 push 重排的关键源码部分,第一个参数是容器的首元素迭代器,第二个参数是洞的初始位置,第三个参数是堆顶,即容器第一个位置,第四个参数是新添加的值,第五个参数是用于进行比较操作的函数对象。从源码能够看出,在比较操作的是时候,父结点作为了运算符的左侧运算对象,父结点比新加的结点小才会进行相应的操作,也就不难理解为什么 less 比较操作维护的却是大堆顶。

template<typename _RandomAccessIterator,typename _Distance, typename _Tp,typename _Compare>
void __push_heap(_RandomAccessIterator __first,_Distance __holeIndex,_Distance __topIndex,_Tp __value,_Compare& __comp) {_Distance __parent = (__holeIndex - 1) / 2; // 计算父结点索引// 比较父结点和要push的值大小关系while (__holeIndex > __topIndex && __comp(__first + __parent, __value)) {*(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first + __parent));__holeIndex = __parent;__parent = (__holeIndex - 1) / 2;}// 将要push的值添加到洞里面*(__first + __holeIndex) = _GLIBCXX_MOVE(__value);}

priority_queue 的 pop 操作

priority_queuepush 操作本质上就是二叉堆有序化的下沉,pop 操作步骤如下:

  1. 首先记录容器最后一个位置的元素值;
  2. 将堆顶的元素移动到容器最后一个位置上,这样堆顶的位置就相当于形成了一个洞;
  3. 然后调整堆,调整堆的过程大致如下:
    • 首先根据堆顶找到其右孩子结点,然后比较右孩子结点和左孩子结点的值,将两者中较大的值放置到堆顶;
    • 被移走的数值则形成了一个洞,因此需要继续向下比较,直到最后不再更新洞的位置
  4. 将之前记录的最后一个位置的元素值 push 到最后一个孔的位置;
  5. 最后从容器中移除最后一个位置的元素。

示例:

下面是 pop 调整堆的关键源码部分,第一个参数是容器的首元素迭代器,第二个参数是洞的初始位置,第三个参数是容器中除去要移除的元素后剩余的个数,第四个参数记录的pop前容器最后一个位置的元素,第五个参数是用于进行比较操作的函数对象。

template<typename _RandomAccessIterator, typename _Distance,typename _Tp, typename _Compare>
void __adjust_heap(_RandomAccessIterator __first,_Distance __holeIndex,_Distance __len,_Tp __value,_Compare __comp)
{const _Distance __topIndex = __holeIndex;_Distance __secondChild = __holeIndex;while (__secondChild < (__len - 1) / 2){// 计算某个父结点的右孩子结点索引__secondChild = 2 * (__secondChild + 1);// 比较左右两个孩子结点的大小,若左孩子结点大则更新索引if (__comp(__first + __secondChild,__first + (__secondChild - 1)))__secondChild--;*(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first + __secondChild));__holeIndex = __secondChild;}// 剩余元素个数为偶数,说明完全二叉树的最后一个位置不是右孩子结点,就只能是左孩子结点// 而如果此时的洞是最后一个位置的父结点,那么就只能将左孩子结点的值移动到父结点处。if ((__len & 1) == 0 && __secondChild == (__len - 2) / 2){__secondChild = 2 * (__secondChild + 1);*(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first+ (__secondChild - 1)));__holeIndex = __secondChild - 1;}__decltype(__gnu_cxx::__ops::__iter_comp_val(_GLIBCXX_MOVE(__comp)))__cmp(_GLIBCXX_MOVE(__comp));// 将之前记录的容器最后一个位置的值填入洞中std::__push_heap(__first, __holeIndex, __topIndex,_GLIBCXX_MOVE(__value), __cmp);
}

原文地址

C++ STL实现的优先队列( priority_queue )相关推荐

  1. C++ STL中的优先队列(priority_queue)使用

    原文:https://www.cnblogs.com/cielosun/p/5654595.html 今天讲一讲优先队列(priority_queue),实际上,它的本质就是一个heap,我从STL中 ...

  2. 浅谈C++ STL中的优先队列(priority_queue)

    2019独角兽企业重金招聘Python工程师标准>>> 从我以前的博文能看出来,我是一个队列爱好者,很多并不是一定需要用队列实现的算法我也会采用队列实现,主要是由于队列和人的直觉思维 ...

  3. 优先队列priority_queue 用法详解

    优先队列priority_queue 用法详解 优先队列是队列的一种,不过它可以按照自定义的一种方式(数据的优先级)来对队列中的数据进行动态的排序 每次的push和pop操作,队列都会动态的调整,以达 ...

  4. STL(七)——队列queue优先队列priority_queue

    一.queue 1.特点:先进先出(FIFO-first in first out) 只允许在表的前端(front,称为队头)进行删除操作,在表的后端(rear,称为队尾)进行插入操作 2.基本操作 ...

  5. C++STL 优先队列priority_queue使用

    头文件:#include <queue> 一.申明方式 std::priority_queue<T> q; std::priority_queue<T, std::vec ...

  6. ZOJ 2849 Attack of Panda Virus (优先队列 priority_queue)

    优先队列,据说标程是并查集,没思路.貌似优先队列都是直接用stl写的,又逼我用stl了.prioriry_queue不熟. ps: value值越小,优先级越高.所以重载 < 运算符时按优先级从 ...

  7. 优先队列(priority_queue)的原理及用法

    一.优先队列的原理及使用 std::priority_queue:在优先队列中,优先级高的元素先出队列,并非按照先进先出的要求,类似一个堆(heap).其模板声明带有三个参数,priority_que ...

  8. c++——优先队列(priority_queue)

    优先队列详解/C++ 优先队列 1.概念:什么是优先队列呢?在优先队列中,元素被赋予优先级,当访问元素时,具有最高级优先级的元素先被访问 .即优先队列具有最高级先出的行为特征.它可以说是队列和排序的完 ...

  9. 【转】c++优先队列(priority_queue)用法详解

    既然是队列那么先要包含头文件#include <queue>, 他和queue不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队 优先队列具有队列的所有特性, ...

最新文章

  1. 用Construct 2制作入门小游戏~
  2. python能在生活中做什么-Python可以解决哪些生活中的小问题
  3. 生物科研神器!30分钟把人家一天的工作都给干完了!
  4. float right不生效_【工具篇】程序员不愿意写 PPT 是姿势不对?
  5. Echarts pie 饼图类型后显示数据
  6. Scratch少儿编程案例~走迷宫游戏
  7. 中国电信-应招知识库(专业知识)
  8. 控制pico—unity中双目摄像机的clearFlags问题
  9. CMOS、TTL门电路基础
  10. HDU_4585_Shaolin
  11. 201521123091 《Java程序设计》第11周学习总结
  12. python pyplot bar 参数_数据可视化之条形图(1):Axes.bar
  13. Python入门(二) part1 列表
  14. 【Ci24R1小尺寸 DFN8/2*2】2.4G双向系统超低成本之选
  15. mysql性能监控 调优_MySQL管理之道:性能调优、高可用与监控(第2版)
  16. 2020第五届上海第二工业大学新生程序设计竞赛(Java题解)
  17. java - (二)netty 心跳监测机制
  18. 发烧? 变蒸?--中医
  19. C语言中strchr和strrchr函数及用法
  20. Python第三方库安装——使用vscode、pycharm安装Python第三方库

热门文章

  1. C++中string与int\double等互转
  2. element组件库中table自定义分页效果
  3. Android开始之 activity_lifecycle和现场保护
  4. 红帽Linux故障定位技术详解与实例(1)
  5. ORACLE EBS常用表及查询语句(最终整理版)
  6. [智能架构系列]什么是Buddy智能开发框架
  7. javascript setTimeout 和 setInterval 区别
  8. [Java核心技术(卷I)] - Java中的参数能做什么和不能做什么
  9. SEO【总结】by 2019年5月
  10. 搭建一个基于http的yum服务器