持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情
💦 priority_queue的介绍
-
优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
-
此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素 (优先队列中位于顶部的元素)。
-
优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue 提供一组特定的成员函数来访问其元素。元素从特定容器的 “ 尾部 ” 弹出,其称为优先队列的顶部。
-
底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
➡ empty():检测容器是否为空。
➡ size():返回容器中有效元素个数。
➡ front():返回容器中第一个元素的引用。
➡ push_back():在容器尾部插入元素。
➡ pop_back():删除容器尾部元素。 -
标准容器类 vector 和 deque 满足这些需求。默认情况下,如果没有为特定的 priority_queue 类实例化指定容器类,则使用 vector。
-
需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数 make_heap、push_heap 和pop_heap 来自动完成此操作。
注意这里的优先级队列不符合队列的先进先出特性,它的 push 没有要求,pop/top 是取优先级最高的,优先级指的是大小,这里默认是大的优先级高,如果要让小的优先级高,就需要仿函数来控制。但要注意并不一定是大的优先级高,小的优先级低,因为对于用数字来评分 (大的高) 是一种场景,对于用字母来评分 (小的高) 又是一种场景。
💦 priority_queue的使用
优先级队列默认使用 vector 作为其底层存储数据的容器,在 vector 上又使用了堆算法将 vector 中元素构造成堆的结构,因此 priority_queue 就是堆,所有需要用到堆的位置,都可以考虑使用 priority_queue。注意:默认情况下 priority_queue 是大堆。
| 函数声明 | 接口说明 |
|---|---|
| priority_queue()/priority_queue(first,last) | 构造一个空的优先级队列 |
| empty() | 检测优先级队列是否为空,是返回 true,否则返回 false |
| top() | 返回优先级队列中最大 (最小) 元素,即堆顶元素 |
| push(x) | 在优先级队列中插入元素 x |
| pop() | 删除优先级队列中最大 (最小) 元素,即堆顶元素 |
#include<iostream>
#include<queue>
#include<functional> //greater的头
using namespace std;
void test_priority_queue()
{
//priority_queue<int> pq;//默认是大堆,大的优先级高
priority_queue<int,vector<int>, greater<int>> pq;//默认是小堆,小的优先级高。控制大小堆的是第3个参数,你要传第3个参数,必须先传第2个参数,因为它也是缺省的
pq.push(1);
pq.push(10);
pq.push(11);
pq.push(3);
pq.push(5);
pq.push(8);
while(!pq.empty())
{
//取堆顶的数据
cout << pq.top() << " ";
//出堆顶(与最后元素交换,再删除它),向下调整
pq.pop();
}
cout << endl;
}
int main()
{
test_priority_queue();
return 0;
}
💦 priority_queue的OJ
1、数组中的第K个最大元素<难度系数⭐⭐>
📝 题述:给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
💨示例1:
输入:[3,2,1,5,6,4] 和 k = 2 输出:5
💨示例2:
输入:[3,2,3,1,2,4,5,5,6] 和 k = 4 输出:4
⚠ 提示:
- 1 <= k <= nums.length <= 10^4^
- -10^4^ <= nums[i] <= 10^4^
🧷 平台:Visual studio 2017 && windows
🔑 核心思想:这道题是一道 top-k 的变形问题。这里用 C++ 来做就非常简单,有如下方案:
-
先调用 <algorithm> 中的 sort 对数组排升序,随后算倒数第 k 大就是正数的 nums.size() - k 为下标的位置。当然直接排降序也可以,其底层是快排。时间复杂度为 O(N*logN)。无空间复杂度。
-
用 priority_queue 建一个大堆,然后pop k - 1 个,再 top() 就是第 k 大的。把 nums 的数据往 maxHeap 里放有 2 种方式。时间复杂度为 O(N + K * logN),N 是向下调整建堆,K * logN 是 pop。空间复杂度 O(N)。 ➡ 遍历nums,一个个 push。 ➡ 构造函数里提供迭代器区间初始化。
-
建一个 k 个数的小堆,其它数依次比较,它比堆顶的数要大,那么就 pop 堆顶,再 push 这个数,最后堆顶的数据就是第 k 大的。时间复杂度是 O(K + (N - K) * logK),空间复杂度是 O(K)。就目前的数据量来说,其实第二种方案和第三种方案的效率差不多,但是当 N 远大于 K 时,尤其是大到内存存不下 (方案一、二都不能适用),第三种方案更适用,此时可以认为它的时间复杂度是 O(N - K) ➡ O(N)。
🧿 方案一
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// //RandAccessIterator,sort默认是升序
// sort(nums.begin(), nums.end());
// //此时倒数第k大就是正数的nums.size()-k为下标的位置。
// return nums[nums.size() - k];
//sort排降序
//greater<int> g;
//sort(nums.begin(), nums.end(), g);
sort(nums.begin(), nums.end(), greater<int>());//同上,匿名对象
//此时k-1作为下标就是第k大的
return nums[k - 1];
}
};
🧿 方案二
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int> maxHeap(nums.begin(), nums.end());
while(k-=1)//同k-=1
{
maxHeap.pop();
}
//此时堆顶既是第k大
return maxHeap.top();
}
};
🧿 方案三
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
//建k个数的小堆
priority_queue<int, vector<int>, greater<int>> kMinHeap;
for(size_t i = 0; i < k; ++i)
{
kMinHeap.push(nums[i]);
}
//再用剩余的数比较
for(size_t j = k; j < nums.size(); ++j)
{
if(kMinHeap.top() < nums[j])
{
kMinHeap.pop();
kMinHeap.push(nums[j]);
}
}
//此时的堆顶就是第k大的
return kMinHeap.top();
}
};
💦 priority_queue的模拟实现
💨PriorityQueue.h
#pragma once
#include<vector>
#include<list>
#include<iostream>
using namespace std;
namespace bit
{
//仿函数/函数对象——自定义类型,类型的对象可以像函数一样使用
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
//模板参数->类型
//函数参数->对象
//less->大堆 greater->小堆,默认是Less
template<class T, class Container = vector<T>, class Compare = Less<T>>
class priority_queue
{
public:
priority_queue()
{}
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)//可以传任意类型的迭代器
{
//插入数据
while(first != last)
{
_con.push_back(*first);
++first;
}
//建堆
for(int i = (_con.size() - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(i);
}
}
void AdjustUp(size_t child)
{
Compare com;
size_t parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else//父亲>=孩子
{
break;
}
}
}
void AdjustDown(size_t parent)
{
Compare com;
size_t child = parent * 2 + 1;
while (child < _con.size())
{
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
++child;
}
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
const T& top() const
{
return _con[0];
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;//构造析构等可以不写,因为这是自定义类型
};
void test_priority_queue()
{
//priority_queue<int> pq;
priority_queue<int, vector<int>, Greater<int>> pq;
pq.push(1);
pq.push(10);
pq.push(11);
pq.push(3);
pq.push(5);
pq.push(8);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
list<int> lt = { 2, 5, 1, 3, 4 };
priority_queue<int, vector<int>, greater<int>> pq1(lt.begin(), lt.end());
while (!pq1.empty())
{
cout << pq1.top() << " ";
pq1.pop();
}
cout << endl;
}
}
💨test.cpp
int main()
{
bit::test_priority_queue();
return 0;
}
📝说明
-
List item
在以前我们要让大堆变小堆,都是直接去改符号,那如何能不改符号的情况下就能达到效果呢 ❓
C语言其实可以利用函数指针来解决。但是 C++ 放弃用函数指针的方式,且非常不建议用函数指针,因为比较复杂。可以说 C++ 里的仿函数/函数对象就是为了替代 C语言里的函数指针。
“ () ” 的功能可以提高优先级、强制类型转换、函数名 (形参表),仿函数用的是函数名 (形参表)。我们实现两个类,分别重载 “ () ” 完成比较大小功能,然后用 priority_queue 这个类来实例化控制。
-
List item
对比 priority_queue 的仿函数和 <algorithm> 中 sort 的仿函数 ❗
priority_queue 是一个类,最好通过模板参数来传递,传的是类型。
这里 less 中给的是 typename Container::value_type,并没有很特别的原因,less 里可以直接给 T,因为它是取 Container 中的 value_type,value_type 是被 typedef 的第一个模板参数,也就是 T。我们要去 Container 中取 value_type。Container 是一个模板,不知道它具体是啥,因为 vector 没有被实例化,所以去取可能会报错,所以加上 typename 是告诉编译器,后面是一个类型,当然报错与否,主要看编译器。这里后面遇到合适的场景会再谈。
sort 是一个函数,最好通过参数来传递。
类模板是显示实例化的,所以说我们可以显示的指定参数;但是函数模板一般是通过实参去推演的,所以说 sort 得写在函数参数里。所以 priority_queue 是传类型,sort 是传对象。
💦 仿函数的变异玩法
要求往优先级队列里放一个日期类,且取出最大的日期 ❓
🧿 方案一:
#include<queue>
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 2022, int month = 5, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day << endl;
return _cout;
}
int main()
{
//我们可以放int,也可以放类
priority_queue<Date> pq;
pq.push(Date(2021, 11, 24));
pq.push(Date(2021, 10, 24));
pq.push(Date(2021, 12, 24));
pq.push(Date(2022, 1, 24));
//pq是自定义类型,所以上面需要重载<<,要取最大的日期,需要重载<
cout << pq.top() << endl;
pq.pop();
return 0;
}
🧿 方案二:
假设极端情况,如果存的是日期类指针呢,如果再用上面的方式,取不出来,因为它比的是地址。解决方案就是我们自己实现仿函数来控制。
#include<queue>
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 2022, int month = 5, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d);
friend class PDateLess;
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day << endl;
return _cout;
}
class PDateLess
{
public:
bool operator()(const Date* p1, const Date* p2)
{
/*if(p1->_year < p2->_year)
|| (p1->_year == p2->_year && p1->_month < p2->_month)
|| (p1->_year == p2->_year && p1->_month == p2->_month && p1->_day < p2->_day)
{
return true;
}
else
{
return false;
}*/
//同上,重载了<的情况
return *p1 < *p2;
}
};
int main()
{
priority_queue<Date*, vector<Date*>, PDateLess> pq;
pq.push(new Date(2023, 11, 24));
pq.push(new Date(2021, 10, 24));
pq.push(new Date(2021, 12, 24));
pq.push(new Date(2022, 1, 24));
cout << (*pq.top()) << endl;
pq.pop();
return 0;
}
最后这里想说明的是我们可以通过仿函数来控制比较方式。