• C++ STL详解(五) -------- priority_queue


    目录

    1. priority_queue介绍

    2.堆的向上和向下调整算法

    (1)堆的向上调整算法

    (2)堆的向下调整算法

    3.priority_queue模拟实现 


                    

     

    1. priority_queue介绍

    ①. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。  

    ②. 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。

    ③. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。

    ④. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:

    • empty():检测容器是否为空
    • size():返回容器中有效元素个数
    • front():返回容器中第一个元素的引用
    • push_back():在容器尾部插入元素
    • pop_back():删除容器尾部元素

    ⑤. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。

    ⑥. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。

            

            

    2.堆的向上和向下调整算法

    • priority_queue的底层实际上就是堆结构

    (1)堆的向上调整算法

    调整的基本思想如下:

    • 将目标结点与其父结点进行比较。
    • 若目标结点的值比父结点的值大,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整;若目标结点的值比其父结点的值小,则停止向上调整,此时该树已经是大堆了。 
    • 以大堆为例,堆的向上调整算法就是在大堆的末尾插入一个数据后,经过一系列的调整,使其仍然是一个大堆
    • 现在有一些数据(49  28  57  34  53  84  83  74  52),经过向上调整之后的顺序是 (84  74  83   53  34  49  57  28  52) 
    • 现在我们要插入一个数据80,下面是调整过程

     代码实现:

    1. //堆的向上调整(大堆)
    2. void AdjustUp(vector<int>& v, int child)
    3. {
    4. size_t parent = (child - 1) / 2; //通过child计算parent的下标
    5. while (child > 0)//调整到根结点的位置截止
    6. {
    7. if (v[parent] < v[child])//孩子结点的值大于父结点的值
    8. {
    9. //将父结点与孩子结点交换
    10. swap(v[child], v[parent]);
    11. //继续向上进行调整
    12. child = parent;
    13. parent = (child - 1) / 2;
    14. }
    15. else//已成堆
    16. {
    17. break;
    18. }
    19. }
    20. }

                             

    (2)堆的向下调整算法

    • 以大堆为例,使用堆的向下调整算法有一个前提,就是待向下调整的结点的左子树和右子树必须都为大堆。

    调整的基本思想如下:

    • 将目标结点与其较大的子结点进行比较。
    • 若目标结点的值比其较大的子结点的值小,则交换目标结点与其较大的子结点的位置,并将原目标结点的较大子结点当作新的目标结点继续进行向下调整;若目标结点的值比其较大子结点的值大,则停止向下调整,此时该树已经是大堆了。 
    • 现在我们要获取priority_queue 的堆顶元素( 84 ),然后pop,此时堆需要重新调整成大堆,
    • 调整过程如下: 先返回堆顶元素,然后将vector 中的第一个元素和最后一个元素的值交换, 然后vector 进行 pop_back().

     代码实现:

    1. //堆的向下调整(大堆)
    2. void AdjustDown(vector<int>& v, int n, int parent)
    3. {
    4. //child记录左右孩子中值较大的孩子的下标
    5. int child = 2 * parent + 1;//先默认其左孩子的值较大
    6. while (child < n) //child不能越界
    7. {
    8. if (child + 1 < n && v[child] < v[child + 1])//右孩子存在并且右孩子比左孩子还大
    9. {
    10. child++;//较大的孩子改为右孩子
    11. }
    12. if (v[parent] < v[child])//左右孩子中较大孩子的值比父结点还大
    13. {
    14. //将父结点与较小的子结点交换
    15. swap(v[child], v[parent]);
    16. //继续向下进行调整
    17. parent = child;
    18. child = 2 * parent + 1;
    19. }
    20. else//已成堆
    21. {
    22. break;
    23. }
    24. }
    25. }

                     

                            

    3.priority_queue模拟实现 

    1. namespace XM //防止命名冲突
    2. {
    3. //比较方式(使内部结构为大堆)
    4. template<class T>
    5. struct less
    6. {
    7. bool operator()(const T& x, const T& y)
    8. {
    9. return x < y;
    10. }
    11. };
    12. //比较方式(使内部结构为小堆)
    13. template<class T>
    14. struct greater
    15. {
    16. bool operator()(const T& x, const T& y)
    17. {
    18. return x > y;
    19. }
    20. };
    21. //优先级队列的模拟实现
    22. template<class T, class Container = vector, class Compare = less>
    23. class priority_queue
    24. {
    25. public:
    26. //堆的向上调整
    27. void AdjustUp(int child)
    28. {
    29. int parent = (child - 1) / 2; //通过child计算parent的下标
    30. while (child > 0)//调整到根结点的位置截止
    31. {
    32. if (_comp(_con[parent], _con[child]))//通过所给比较方式确定是否需要交换结点位置
    33. {
    34. //将父结点与孩子结点交换
    35. swap(_con[child], _con[parent]);
    36. //继续向上进行调整
    37. child = parent;
    38. parent = (child - 1) / 2;
    39. }
    40. else//已成堆
    41. {
    42. break;
    43. }
    44. }
    45. }
    46. //插入元素到队尾(并排序)
    47. void push(const T& x)
    48. {
    49. _con.push_back(x);
    50. AdjustUp(_con.size() - 1); //将最后一个元素进行一次向上调整
    51. }
    52. //堆的向下调整
    53. void AdjustDown(int n, int parent)
    54. {
    55. int child = 2 * parent + 1;
    56. while (child < n)
    57. {
    58. if (child + 1 < n&&_comp(_con[child], _con[child + 1]))
    59. {
    60. child++;
    61. }
    62. if (_comp(_con[parent], _con[child]))//通过所给比较方式确定是否需要交换结点位置
    63. {
    64. //将父结点与孩子结点交换
    65. swap(_con[child], _con[parent]);
    66. //继续向下进行调整
    67. parent = child;
    68. child = 2 * parent + 1;
    69. }
    70. else//已成堆
    71. {
    72. break;
    73. }
    74. }
    75. }
    76. //弹出队头元素(堆顶元素)
    77. void pop()
    78. {
    79. swap(_con[0], _con[_con.size() - 1]);
    80. _con.pop_back();
    81. AdjustDown(_con.size(), 0); //将下标为0个元素进行一次向下调整
    82. }
    83. //访问队头元素(堆顶元素)
    84. T& top()
    85. {
    86. return _con[0];
    87. }
    88. const T& top() const
    89. {
    90. return _con[0];
    91. }
    92. //获取队列中有效元素个数
    93. size_t size() const
    94. {
    95. return _con.size();
    96. }
    97. //判断队列是否为空
    98. bool empty() const
    99. {
    100. return _con.empty();
    101. }
    102. private:
    103. Container _con; //底层容器
    104. Compare _comp; //比较方式
    105. };
    106. }

    (1)关于priority_queue底层堆的调整

    • push : 在容器尾部插入元素后进行一次向上调整算法
    • pop : 将容器头部和尾部元素交换,再将尾部元素删除,最后从根结点开始进行一次向下调整算法
    • 通过push 和 pop 函数底层堆不断进行调整始终保持堆的结构 ;每插入 / 删除 一个元素都要进行堆的调整 

    (2)仿函数实现比较方式

    • 为什么要用仿函数 , 而不用函数指针 , 因为函数指针太复杂了,用起来不好
    • 通过重载operator()方式实现仿函数
    • 默认情况下,priority_queue是大堆
    • 如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载。
  • 相关阅读:
    Vue3+TSX开发模式下内置组件的替代方案
    JAVA8新特性- 函数式接口
    Docker挂载镜像到本地(日常记录)
    C语言:空指针野指针
    SCT1270,SCT1271,12.6V, 7A, 全集成高效升压变换器
    c# 异步进阶————channel [一]
    搜维尔科技:Varjo-最自然和最直观的互动
    论文笔记:Large Language Model for Participatory Urban Planning
    JPA常见异常 JPA可能抛出的异常
    CMU15445 (Fall 2019) 之 Project#4 - Logging & Recovery 详解
  • 原文地址:https://blog.csdn.net/m0_52169086/article/details/126347964