目录
C++标准库不只是包含了顺序容器,还包含一些为满足特殊需求而设计的容器,它们提供简单的接口。
这些容器可被归类为容器适配器(container adapter),它们是改造别的标准顺序容器,使之满足特殊需求的新容器。
适配器:也称配置器,把一种接口转为另一种接口。
有三种标准的 容器适配器: stack(栈)、queue(队列)和priority queue(优先级队列)。 priority queue就是“根据排序准则,自动将元素排序”的队列,其所谓“下一个””元素总是拥有最高优先级。
priority queue优先级队列
priority queue优先级队列,它的接口和queue非常相似,有入队push(),获取第一个元素top(),pop删除第一个元素。这里的第一个元素并不是第一个放入的元素,而是"优先级最高"的元素。
priority queue内的元素已经根据其值进行了排序。当然你可以通过参数指定一个排序准则,默认的排序是升序排列。所谓的第一个元素就是当前"数值最大的元素"(默认的vector,第一个"最大值"元素保存在最后,因为vector尾删效率高)。如果同时存在多个数值最大的元素,我们无法确定哪一个在前哪一个在后。
其内部数据结构为:大根堆
不能使用list作为其容器
priority queue也是定义在头文件中
#include <queue>
namespace std {
template < typename T,
typename Container=vector<Type>,
typename Compare=less<typename Container::value_type> >
class priority_queue;
}
第一个参数T是元素类型,第二个参数为内部存放元素的容器,默认为vector,第三个参数是排序规则,默认是升序(less)。下面定义一个int的priority queue。
priority_queue<int> pq;//定义一个int的优先级队列
实际上priority queue优先级队列只是简单的把各项操作转为内部容器对应的函数调用。你当然可以使用支持随机迭代器(需要排序),front(),push_back(),pop_back()等操作的标准容器作为内部容器。例如可以使用deque作为内部容器。
priority_queue<int,deque<int>> pq2;//创建一个int的优先级队列,并使用deque作为存放数据的容器
当然你也可以定义自己的排序准则,例如下面使用一个降序排序规则。
priority_queue<int,vector<int>,greater<int>> pq3;//创建一个int的优先级,使用vector存放数据,并按降序排列
在上面的priority queue pq3中,"下一个元素"始终是元素值最小的(降序最后一个元素,值最小)。
定义及初始化
priority queue的初始化方式比较丰富,具体如下:
//优先级队列
#include <queue>
#include <iostream>
using namespace std;
int main()
{
priority_queue <int> q1;//创建一个保存int的空优先级队列
if (q1.empty())
cout << "q1是空的,它的数据个数为:" << q1.size() << endl;
//创建一个保存int,且用deque保存内部数据的优先级队列(默认为升序)
priority_queue <int, deque <int> > q2;
q2.push(5); //入队
q2.push(15);//入队
q2.push(10);//入队
cout << "q2的数据:";
while (!q2.empty())//只要q2不为空,循环继续
{
cout << q2.top() << " "; //输出第一个元素的值(最大)
q2.pop(); //删除第一个元素
}
cout << endl;
//创建保存int,用vector保存内部数据且降序的优先级队列
priority_queue <int, vector<int>, greater<int> > q3;
q3.push(2); //入队
q3.push(1);//入队
q3.push(3);//入队
cout << "q3的数据:";
while (!q3.empty())
{
cout << q3.top() << " ";//输出第一个元素的值(最小)
q3.pop(); //删除第一个元素
}
cout << endl;
q1.push(100);
q1.push(200);
//利用q1创建一个新的优先级队列q4
priority_queue <int> q4(q1);
cout << "q4 的数据:";
while (!q4.empty())
{
cout << q4.top() << " ";
q4.pop();
}
cout << endl;
// 利用迭代器创建优先级队列
vector <int> v5{ 10,30,20,40 };
priority_queue <int> q5(v5.begin(), v5.end());
cout << "q5的数据:";
while (!q5.empty())
{
cout << q5.top() << " ";
q5.pop();
}
cout << endl;
return 0;
}
优先级队列的工作原理
元素优先级的确定
在优先级队列中,确定元素优先级的方式多种多样,具体取决于应用场景。常见的方式有:
- 数值大小:例如,在一个任务调度系统中,任务被赋予一个表示紧急程度的数值,数值越小优先级越高。假设任务 A 的紧急程度为 1,任务 B 的紧急程度为 3,那么任务 A 的优先级高于任务 B。
- 特定属性:在一个物流配送系统中,根据包裹的类型(如易碎品、加急件等)来确定优先级。易碎品和加急件通常具有较高优先级,需要优先处理和配送。
- 自定义规则:在复杂的游戏 AI 系统中,可能根据游戏角色的生命值、攻击力以及当前任务的重要性等多个因素,通过自定义的算法来综合计算角色的行动优先级。
元素的入队与出队
入队操作:当一个新元素要进入优先级队列时,系统会根据其优先级将它插入到合适的位置。这与普通队列直接在队尾添加元素不同。例如,在一个基于堆实现的优先级队列中,新元素会被添加到堆的末尾,然后通过一系列的比较和交换操作,上浮到符合其优先级的位置,以确保整个队列的优先级顺序。
出队操作:优先级队列总是移除当前队列中优先级最高的元素。同样以堆实现为例,堆顶元素即为优先级最高的元素,出队时将堆顶元素移除,然后把堆的最后一个元素移动到堆顶,再通过下沉操作重新调整堆结构,保证堆顶始终是最高优先级元素。
优先级队列的实现方式
堆(Heap)实现 堆是实现优先级队列最常用的数据结构之一。堆是一种特殊的完全二叉树,分为最大堆和最小堆。
下面是queue文件部分源码:
template <class _Ty, class _Container = vector<_Ty>,
class _Pr = less<typename _Container::value_type>>
class priority_queue
{
protected:
_Container c{};//容器
_Pr comp{}; //比较函数对象
public:
template <class _Alloc, enable_if_t<uses_allocator_v<_Container,_Alloc>, int>= 0>
priority_queue(const _Pr& _Pred, const _Container& _Cont, const _Alloc& _Al) :
c(_Cont, _Al), comp(_Pred)//构造函数
{
make_heap(c.begin(), c.end(), comp);//变成堆(默认为大根堆)
}
void push(const value_type& _Val) {
c.push_back(_Val);
push_heap(c.begin(), c.end(), comp);//往堆增加一个元素
}
void pop() {
pop_heap(c.begin(), c.end(), comp);//从堆中删除一个元素
c.pop_back();
}
}
- 最大堆:在最大堆中,每个节点的值都大于或等于其左右子节点的值。因此,堆顶元素即为整个堆中的最大值,适用于实现优先级高的元素先出队的优先级队列。例如,在一个任务调度系统中,如果任务的优先级用数值表示,数值越大优先级越高,那么可以使用最大堆来实现优先级队列。
- 最小堆:与最大堆相反,最小堆中每个节点的值都小于或等于其左右子节点的值。堆顶元素是整个堆中的最小值,适用于需要低优先级元素先出队的场景。比如,在一个网络路由算法中,根据路径的开销来确定优先级,开销越小优先级越高,此时使用最小堆实现优先级队列可以高效地选择开销最小的路径。
常用操作
priority queue优先级队列的操作比较简单,只要几个简单的成员函数。
empty成员函数 判断优先级队列是否为空。是返回true,不是返回false。
pop成员函数 把第一个元素从队列删除。
push成员函数 往优先级队列中插入一个数据。
size成员函数 返回优先级队列数据个数。
top成员函数 返回第一个元素的引用。
#include <queue>
#include <iostream>
using namespace std;
int main()
{
priority_queue<int> q; //创建一个空优先级队列
if (q.empty())
cout << "q是一个空队" << endl;
q.push(3);
q.push(2);
q.push(5);
q.push(4);
q.push(1);
cout << "插入5个数据之后,当前的数据个数为" << q.size() << endl;
cout << "依次出队的数据为:";
while (!q.empty())//只要q1不为空,循环继续
{
cout << q.top() << " ";
q.pop();
}
return 0;
}
优先级队列的应用场景
任务调度系统
在操作系统和分布式系统中,任务调度是一个关键环节。优先级队列在任务调度系统中发挥着核心作用。例如,在一个多核处理器的操作系统中,有多个任务等待执行,包括系统任务、用户应用任务等。系统会根据任务的类型(如前台应用任务优先级高于后台任务)、紧急程度(如处理硬件中断的任务具有最高优先级)等因素为任务分配优先级,并将任务放入优先级队列中。处理器从优先级队列中依次取出优先级最高的任务执行,确保系统资源优先分配给最重要、最紧急的任务,从而提高系统的整体性能和响应速度。
事件驱动的模拟
在事件驱动的模拟系统中,如交通模拟、物流模拟等,优先级队列用于管理和调度事件。例如,在交通模拟中,车辆的到达、离开路口,信号灯的切换等都可以看作是事件。每个事件都有一个发生的时间和优先级。优先级队列按照事件发生的时间顺序(时间越早优先级越高)存储事件,模拟系统每次从队列中取出下一个即将发生的事件进行处理,更新模拟状态,从而逼真地模拟现实世界中的动态过程。
最短路径算法
在图论算法中,许多最短路径算法依赖优先级队列来提高效率。以 Dijkstra 算法为例,该算法用于在带权有向图中寻找从一个源节点到其他所有节点的最短路径。在算法执行过程中,使用优先级队列来存储节点及其到源节点的当前最短距离。每次从优先级队列中取出距离源节点最近(即优先级最高)的节点,更新其邻接节点到源节点的距离,并将更新后的节点重新插入优先级队列。通过这种方式,Dijkstra 算法能够高效地找到最短路径,相比于不使用优先级队列的朴素算法,时间复杂度从 O (V^2) 降低到 O ((V + E) log V),其中 V 是图中节点的数量,E 是边的数量。
数据压缩算法
在一些数据压缩算法,如 Huffman 编码中,优先级队列用于构建 Huffman 树。Huffman 编码是一种根据字符出现频率进行压缩的算法。在构建 Huffman 树时,将每个字符及其出现频率作为一个节点放入优先级队列中,频率越低优先级越低。每次从队列中取出两个优先级最低的节点,合并成一个新节点,新节点的频率为两个子节点频率之和,并将新节点重新插入优先级队列。重复这个过程,直到队列中只剩下一个节点,这个节点就是 Huffman 树的根节点。通过构建 Huffman 树,可以为每个字符生成最优的编码,实现数据的高效压缩。
总结
优先级队列作为一种特殊且强大的数据结构,打破了传统队列先进先出的规则,根据元素的优先级进行智能调度。通过多种实现方式,如堆、二叉搜索树和链表,优先级队列能够适应不同场景的需求。在任务调度、事件模拟、最短路径算法以及数据压缩等众多领域,优先级队列都展现出了卓越的性能和不可或缺的价值。深入理解优先级队列的概念、原理、实现方式及其应用场景,将为我们在算法设计、系统开发等方面提供有力的工具和全新的思路,助力我们解决各种复杂的实际问题,在计算机科学的道路上迈出更加坚实的步伐。