优先级队列 Priority_queue(搬运)
链接:https://www.nowcoder.com/discuss/667873?type=1&order=2&page=0&channel=-1&source_id=discuss_center_1_nctrack
来源:牛客网
写在前面:
容器适配器是STL中重要的组成部分,栈(stack),队列(queue),优先级队列(priority_queue)是最常见的容器适配器。熟悉priority_queue的伙伴应该都知道,优先级队列的出队顺序实际上是逆优先级的,说人话就是如果判断函数是less
(a < b = True 则a优先级高于b)出队顺序是降序,即低优先级的元素反而先出队。虽然熟记这一规律并不影响使用,但是为什么是这样这一问题属实折磨了我很久,网上也很难搜到这一小点的资料,故查阅资料写下此文以解决自己的强迫症。
<CPP primer>这一神书中对于优先级队列有真的有这样一段描述「priority_queue允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比他低的已有元素之前。」但是我们实际使用过后会发现,例如对于less这一运算符,数字越小优先级越高,如果我们插入1~>5,那么队列顺序应该是[1,2,3,4,5],这样符合书中的描述。但是根据尾进头出的队列特点,出队顺序也应该是1->5,但是熟悉优先级队列的小伙伴都知道默认的less运算符出队顺序是降序也就是5->1。
下面让我们从priority_queue的定义聊起,出队顺序与常识理解反过来的原因
优先级队列 Priority_queue
这是一个拥有权值queue,其内部元素按照元素的权值排列。可实现O(logN)时间复杂度实现插入删除,O(1)时间复杂度取最高权重值。通过一些骚操作可以实现O(1)时间复杂度取中位数等操作。
权值较高者排在最前优先出队。其中缺省情况下系统是通过一个max-heap以堆实现完成排序特性,表现为一个以vector表现的完全二叉树。
定义
priority_queue<type, container, compare>
其中Type代表数据类型,Container代表容器类型,缺省状态为vector; Compare是比较方式,默认采用的是大顶堆(less<T>)。
1
2
3
4
|
//降序队列 大顶堆 less 大到小 默认 priority_queue < int ,vector< int >,less< int > pq; //升序队列 小顶堆 great 小到大 priority_queue < int ,vector< int >,greater< int > pq; |
包含的方法
- top() 访问队头
- empty()
- size()
- push() / emplace
- pop()
比较函数(主要讲仿函数已了解可跳过)
首先让我们了解一下比较函数,即less
与greater
是什么。
less<int>看似实现的是一个函数的功能,你甚至可以通过less<int>(a,b)
调用他去比较a和b的大小。但实际上less<int>
是通过一个类重载()
实现类似函数调用的功能的,这被称为仿函数
std的比较函数
std
已定义好了自带greater<int>
和less<int>
两个比较方法。他们的实现方式是仿函数和类模板。
仿函数的理解
仿函数主要就是通过对类中operator()
进行实现,使得类的实例能够实现类似函数的调用操作,这个类就有了类似函数的行为,就是一个仿函数类了,下面是个简单例子。
1
2
3
4
5
6
7
8
9
10
11
12
|
class test{ void operator()( int x){ cout<<x<<endl; } } int main() { test t; t( 10 ); return 0 ; } //输出10 |
再增加上模版相关的特点,增强泛化能力我们则可以自行实现一个greater
和less
的仿函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
template < class T> struct greater { bool operator() ( const T& x, const T& y) const { return x>y;} typedef T first_argument_type; typedef T second_argument_type; typedef bool result_type; }; template < class T> struct less { bool operator() ( const T& x, const T& y) const { return x<y;} typedef T first_argument_type; typedef T second_argument_type; typedef bool result_type; }; |
特点
- 仿函数是一个类,不是函数
- 仿函数重载了(),使得可以类似调用函数那样调用实例。(所以大小堆的调用是greater<int>() ,就是类似调用函数的,实际上是一个叫greater的模板类,输入的参数类型是int,`operator()是这个模板类的一个函数)
常见的实例
1
2
3
4
5
6
7
8
9
10
|
//升序队列 小顶堆 great 小到大 priority_queue < int ,vector< int >,greater< int > > pq; //升序 //降序队列 大顶堆 less 大到小 默认 priority_queue < int ,vector< int >,less< int > > pq; //降序 实际上是保持优先级最高的元素在[ 0 ]的位置,每次pop或者push操作会更新 声明参数 priority_queue<Type, Container, Funcitonal>; priority_queue<pair< int , int > > pq_pair; //结果先按照pair的first元素降序,first元素相等再按照second元素降序 priority_queue<pair< int , int >,vector<pair< int , int > >,greater<pair< int , int > > > |
为什么sort和priority_queue的顺序不同
可以发现对于sort和priority_queue,使用greater和less类模板是结果不同的。
先上代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
#include <algorithm> #include <iostream> #include <queue> #include<iterator> using namespace std; int main(){ vector< int > vec = { 1 , 9 , 2 , 3 , 4 , 7 , 6 , 5 , 8 }; ostream_iterator< int > output(cout, " " ); cout<< "Vector vec contains: " ; copy(vec.begin(),vec.end(),output); cout<< "\nAfter greater<int>(): \n" ; cout<< "sort: " ; sort(vec.begin(),vec.end(),greater< int >()); //内置类型从大到小 copy(vec.begin(),vec.end(),output); cout << endl; cout<< "priority_queue: " ; priority_queue < int ,vector< int >,greater< int > > pqg; //升序 for (auto i : vec) pqg.push(i); while (!pqg.empty()){ cout << pqg.top() << ' ' ; pqg.pop(); } cout << endl; cout<< "\nAfter less<int>():\n" ; cout<< "sort: " ; sort(vec.begin(),vec.end(),less< int >()); //内置类型小大到大 copy(vec.begin(),vec.end(),output); cout << endl; priority_queue < int ,vector< int >,less< int > > pql; //降序 大->小 for (auto i : vec) pql.push(i); cout<< "priority_queue: " ; while (!pql.empty()){ cout << pql.top() << ' ' ; pql.pop(); } cout << endl; return 0 ; } /*输出 Vector vec contains: 1 9 2 3 4 7 6 5 8 After greater<int>(): sort: 9 8 7 6 5 4 3 2 1 priority_queue: 1 2 3 4 5 6 7 8 9 After less<int>(): sort: 1 2 3 4 5 6 7 8 9 priority_queue: 9 8 7 6 5 4 3 2 1 */ |
可以观察到,都是使用std
的模板类less
和greater
,但是使用sort
与priority_queue
的排序顺序是相反的。
主要原因是priority_queue的内部实现方法是堆,less对应的是大顶堆。在此排序下调用top()得到的是堆顶,也就是取值时是从大到小。
那么问题来了,为什么less需要对应的是大顶堆。less明明代表更小的数具有更高的优先级,这一优先级体现在哪里。
堆的实现
进一步观察堆的内部实现,可以发现堆内部实现依赖vector
,因为堆一定是一个完全二叉树,所以使用下标索引直接进行访问是很方便的,不必存成二叉树的结构,使用vector效率更高。
堆具有以下的特点:
- 可以在O(logN)的时间复杂度内向堆中插入元素;
- 可以在O(logN)的时间复杂度内向堆中删除元素;
- 可以在O(1)的时间复杂度内获取堆顶元素;
堆元素的删除
以{12 10 3.5 6.5 8 2.5 1.5 6}
为例,这个vector画成堆的形式为下图
堆的删除实现操作为,将堆顶元素(12)与最后一个堆中最后元素(6)进行交换,然后删去最后一个元素(完成了堆顶元素出堆),再对当前vector中的第一个元素(6)进行下沉操作,恢复堆的结构
less含义的体现
一个删除元素的例子
例如排好的大顶堆[9,5,7,3]。(9是根节点)
第一次出堆的时候9会被移到最后与3交换位置,同时会对3进行下沉保证堆结构,9出堆。
[3,5,7,9]->[7,5,3,|9] ( | 后的数字代表已经出堆)
第二次出堆 7与3交换位置。( 此时9已经出堆了,只是为了方便后续表达所以保留)
[3,5,|7,9]->[5,3,|7,9]
第三次出堆 5与3交换位置。5出堆
[5,3,|7,9]->[3,|5,7,9]
第四次出堆 3出堆,此时堆为空
[|3,5,7,9]
可以观察到,假如我们将出堆的数保留在vector中的话,数字排序确实是递增的(符合less
的直观性质),但是由于出堆的问题,顺序刚好反了过来,所以(less
对应的是大顶堆,出堆顺序是从大到小)
- n个无序元素构成大顶堆(最后一个节点上浮)
- 根节点和最后一个元素交换
- 剩下n-1个元素重新构成大顶堆(根节点下沉)
- 重复2,3直到数组排序完毕
那么,如果我们在删除堆的时候,省略真实的删除操作,仅仅将其移动到末尾。可以观察到,当堆内元素为空的时候,原数据为{1.5, 2.5, 3.5, 6, 6.5, 8, 10, 12}
。这时的顺序与less
所描述的,元素越小优先级越高相同。所以这就是less含义(数值越小优先级排序越高)的体现。之所以使用中less
代表降序,只是由于出堆这一过程导致了元素出堆反序了,相当于逆序输出。
自定义优先级顺序
逆序输出这一点在自定义优先级顺序中也需要注意。同时自定义比较函数时需要注意。重写的应该是仿函数不是函数,自定义的cmp应该是一个类或者struct。
自定义cmp
注意出堆的原因导致顺序反了过来,如果cmp(a,b) == true
在堆中的排序说明b会在a前面,那么要从小到大,应该用>
符号。自定义cmp中,期望升序,越小越前,大于号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
struct Node{ int x,y; Node( int a = 0 , int b = 0 ): x(a),y(b){} }; struct cmp{ bool operator() (Node a,Node b){ if (a.x == b.x) return a.y>b.y; return a.x>b.x; } }; int main(){ priority_queue<Node, vector<Node>, cmp> pq; for ( int i = 0 ; i < 10 ; ++i){ pq.push(Node(rand(),rand())); } while (!pq.empty()){ cout<<pq.top().x<< ' ' <<pq.top().y<<endl; pq.pop(); } return 0 ; } |
重写operator<
如果希望利用系统自带的仿函数greater
或者less
。其中greater
对应使用的是>
符号,less
使用的是<
符号。了解原理后实际上重载<
或>
均可。
希望通过less
实现,由于less<T>()的实现借助了<,所以可以通过,重写<符号实现
一般来说不推荐改变<号的原有意义,即<
仍使用<
实现,在选择使用less或者greater方法时想好想要的是升序输出还是降序输出。下面的例子中,期望降序输出,所以使用less,重写<操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct Node{ int x,y; Node( int a = 0 , int b = 0 ): x(a),y(b){} }; //需要注意的是priority_queue默认的优先级计算是less<Node>,所以大顶堆应该重写的是operator< //注意返回应该是> 虽然着很奇怪 bool operator<(Node a ,Node b){ if (a.x==b.y) return a.y<b.y; return a.x<b.x; } int main(){ priority_queue<Node> pq; //pq队列是从大到小排序的 return 0 ; } |
反之如果希望借助less ()实现从小到大 排序,则第7 8 行的比较符号应该为>符号。
表格总结
方法或容器 | less<T>()的顺序(默认) | greater<T>()的顺序 | cmp仿函数 大于号>实现 |
---|---|---|---|
priority_queue 全是反过来的 | 降序 大->小 | 升序 小->大 | 升序 小->大 |
sort | 升序 小->大 | 降序 大->小 | 降序 大->小 |
map | 升序 小->大 | 降序 大->小 | 降序 大->小 |
除了priority_queue使用的是堆,导致全部大小比较反了过来,其他均是正常符合逻辑的操作,即判断为func(a,b)判断为true则a在前。只有priority_queue特殊,如果func(a,b)
判断为true,优先级队列中b在前。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话