【c++丨STL】priority_queue(优先级队列)的使用与模拟实现

简介: 本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。

前言

       之前我们学习了STL中的两个容器适配器:stack和queue。本篇文章,我们将学习另一个容器适配器:priority_queue(优先级队列),并尝试模拟实现。


一、priority_queue简介

       优先级队列是一种容器适配器,根据某种严格的弱排序标准,特别设计为它的第一个元素总是它所包含的元素中的最大元素。



       虽然它的名字叫“优先级队列”,但实际上它跟队列没有什么关系,它的底层结构是堆。如果你对堆这一数据结构并不是很了解,可以参阅这篇文章:


https://developer.aliyun.com/article/1635160?spm=a2c6h.24874632.expert-profile.37.6c9229beqQZU05


       既然它的底层结构是一个堆,那么它也就符合堆的所有性质,例如不能遍历、只能从堆顶出入数据等。 使用priority_queue时,需要引头文件<queue>。


       作为一棵顺序存储的完全二叉树,priority_queue也通过复用其他容器来实现(一般是vector),因此作为一个容器适配器而存在。需要特别注意的是,它的模板参数中有一个仿函数,可以用于控制生成大堆或者小堆(默认是大堆)。



话不多说,我们开始逐一介绍它的常用接口。


二、priority_queue的使用

       priority_queue的成员函数如下:



构造函数(constructor)


priority_queue常用的构造函数有两个:


image.png


代码示例:

#include <iostream>
#include <queue>
using namespace std;
 
int main()
{
    priority_queue<int> pq1;//无参构造
 
    vector<int> arr = { 1,2,3,4,5 };
    priority_queue<int> pq2(arr.begin(), arr.end());//迭代器区间构造
    return 0;
}

empty


empty的作用是判断优先级队列是否为空。如果为空,返回true;否则返回false。


代码示例:

#include <iostream>
#include <queue>
using namespace std;
 
int main()
{
    priority_queue<int> pq1;//无参构造
 
    vector<int> arr = { 1,2,3,4,5 };
    priority_queue<int> pq2(arr.begin(), arr.end());//迭代器区间构造
 
    cout << pq1.empty() << endl;
    cout << pq2.empty() << endl;
    return 0;
}


size


size的作用是获取优先级队列中的元素个数。代码示例:

#include <iostream>
#include <queue>
using namespace std;
 
int main()
{
    priority_queue<int> pq1;//无参构造
 
    vector<int> arr = { 1,2,3,4,5 };
    priority_queue<int> pq2(arr.begin(), arr.end());//迭代器区间构造
 
    cout << pq1.size() << endl;
    cout << pq2.size() << endl;
    return 0;
}


top


top函数用于获取队头(堆顶)的元素。代码示例:

#include <iostream>
#include <queue>
using namespace std;
 
int main()
{
    vector<int> arr = { 2,1,3,5,4 };
    priority_queue<int> pq(arr.begin(), arr.end());
 
    cout << pq.top() << endl;
    return 0;
}


push和pop


push的功能是插入一个数据到优先级队列当中。当然,数据插入之后,函数会调用向上调整算法使整个结构保持堆的性质。



pop的功能是删除队头(堆顶)数据。删除之后,函数会调用向下调整算法来保持堆结构的性质。


代码示例:

#include <iostream>
#include <queue>
using namespace std;
 
int main()
{
    priority_queue<int> pq;
 
    pq.push(3);
    pq.push(4);
    pq.push(2);
    pq.push(1);
    pq.push(5);
 
    while (!pq.empty())//非空则循环出数据
    {
        cout << pq.top() << ' ';
        pq.pop();
    }
    cout << endl;
    return 0;
}


swap


swap用于交换两个优先级队列的数据。


仿函数的使用

       之前已经提到,pritority_queue的模板参数中有一个仿函数,可以支持创建大堆或者小堆。STL提供了两个仿函数:less和greater。我们传入less时,生成大堆;greater生成小堆(这里个人认为是设计的一大败笔,与其他容器和算法的使用含义刚好相反)。这里需注意以下几点:


1. priority_queue的模板参数顺序依次是:元素类型、容器、仿函数。所以我们需要显示传仿函数时,需要先传入元素类型和容器的模板参数。


2. 元素类型也要传给容器和仿函数的模板参数。


3. 如果元素是我们自己定义的类,则这些元素之间比较大小的逻辑需要我们自己通过运算符重载去定义,然后将类型传给仿函数。


接下来,我们尝试使用仿函数创建一个大堆和一个小堆:

#include <iostream>
#include <queue>
using namespace std;
 
int main()
{
    priority_queue<int> pq1;//大堆
 
    pq1.push(3);
    pq1.push(4);
    pq1.push(2);
    pq1.push(1);
    pq1.push(5);
 
    cout << "pq1:";
    while (!pq1.empty())
    {
        cout << pq1.top() << ' ';
        pq1.pop();
    }
    cout << endl;
 
    priority_queue<int, vector<int>, greater<int>> pq2;//小堆
 
    pq2.push(3);
    pq2.push(4);
    pq2.push(2);
    pq2.push(1);
    pq2.push(5);
 
    cout << "pq2:";
    while (!pq2.empty())
    {
        cout << pq2.top() << ' ';
        pq2.pop();
    }
    cout << endl;
    return 0;
}


三、priority_queue的模拟实现

       学习了优先级队列的使用之后,我们尝试模拟实现一个优先级队列。


首先是仿函数的实现:

//仿函数
template<class T>
class Less
{
public:
    bool operator()(const T& x, const T& y) const
    {
        return x < y;
    }
};
template<class T>
class Greater
{
public:
    bool operator()(const T& x, const T& y)
    {
        return x > y;
    }
};


在仿函数实现当中,我们首先定义两个类分别重载函数调用操作符,然后在函数内判断大小即可。注意less是小于,greater是大于。


接下来是容器适配器的实现:

template<class T, class Container = vector<T>, class Compare = Less<T>>
class PriorityQueue
{
public:
    //强制生成默认构造
    PriorityQueue() = default;
 
    //迭代器区间构造
    template<class InputIterator>
    PriorityQueue(InputIterator first, InputIterator last)
        :_con(first, last)
    {
        for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)//对区间内的数据从后往前进行向下调整
        {
            AdjustDown(i);
        }
    }
 
    //取堆顶数据
    const T& top() const
    {
        return _con[0];
    }
 
    //判空
    bool empty() const
    {
        return _con.empty();
    }
 
    //入堆
    void push(const T& x)
    {
        _con.push_back(x);
        AdjustUp(_con.size() - 1);
    }
    //向上调整算法
    void AdjustUp(size_t child)
    {
        Compare com;//仿函数定义
        size_t parent = (child - 1) / 2;
        while (child > 0)
        {
            if (com(_con[parent], _con[child]))
            {
                swap(_con[child], _con[parent]);
                child = parent;
                parent = (child - 1) / 2;
            }
            else
            {
                break;
            }
        }
    }
 
    //出堆
    void pop()
    {
        swap(_con[0], _con[_con.size() - 1]);
        _con.pop_back();
        AdjustDown(0);
    }
    //向下调整算法
    void AdjustDown(size_t parent)
    {
        Compare com;//仿函数定义
        size_t child = parent * 2 + 1;
        while (child < _con.size())
        {
            if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
            {
                child++;
            }
            if (com(_con[parent], _con[child]))
            {
                swap(_con[parent], _con[child]);
                parent = child;
                child = parent * 2 + 1;
            }
            else
            {
                break;
            }
        }
    }
private:
    Container _con;//容器
};


可以发现,与stack和queue相同,我们都是在函数体内部调用了封装容器的接口。这里需要注意迭代器区间构造的实现逻辑还有向上/向下调整算法的大小判断,以保证less默认建大堆,greater默认建小堆。


总结

       今天我们学习了STL的第三个容器适配器--priority_queue(优先级队列)的使用以及模拟实现。 如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

目录
打赏
0
1
1
0
138
分享
相关文章
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
29 2
|
12天前
|
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
116 73
|
14天前
|
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
40 3
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
39 16
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
21天前
|
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
67 6
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等