priority_queue使用
http://www.cplusplus.com/reference/queue/priority_queue/ 文档介绍
-
优先级队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的(大堆为例)
-
在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素),
-
优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素,元素从特定容器的“尾部”弹出,其称为优先队列的顶部,
-
底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类,容器应该可以通过随机访问迭代器访问,并支持以下操作:
- empty():检测容器是否为空
- size():返回容器中有效元素个数
- front():返回容器中第一个元素的引用
- push_back():在容器尾部插入元素
- pop_back():删除容器尾部元素
- 标准容器类vector和deque满足这些需求,默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector,
- 需要支持随机访问迭代器,以便始终在内部保持堆结构,容器适配器通过在需要时自动调用算法函make_heap、push_heap和pop_heap来自动完成此操作,
priority_queue介绍
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue,
注意:默认情况下priority_queue是大堆,其使用的仿函数是less
定义方式
第一个参数:类型 第二个参数:容器适配器,默认是vector 第三个参数:比较器(仿函数) 默认是less
对应是大根堆结构
如果想要实现成小根堆结构 ->需要自己传入仿函数greater
->需要引用头文件functional
方式1:容器适配器和仿函数都使用默认值
priority_queue<int> q; //默认是vector作为容器适配器,大根堆结构
//相当于:priority_queue<int, vector<int>, ::less<int>> q; 要指定在哪个域中找仿函数,否则报错
方式2:自己给容器适配器和仿函数
priority_queue<int, vector<int>, ::greater<int>> q2; //是vector作为容器适配器,小根堆结构
注意:默认容器适配器是vector
,默认的仿函数是less
对应大根堆结构
可以使用vector的容器的迭代器区间构造优先级队列:
int main()
{
int a[] = { 3,7,6,5,4 };
int sz = sizeof(a) / sizeof(a[0]);
vector<int> v(a, a + sz);//用数组的一段区间初始化vector容器,指针是原生迭代器
priority_queue<int> q(v.begin(), v.end());//使用迭代器区间初始化优先级队列
while (!q.empty())
{
cout << q.top() << endl; // 7 6 5 4 3 大根堆
q.pop();
}
cout << endl;
return 0;
}
函数接口
成员函数 | 功能 |
---|---|
push | 在优先级队列中插入元素x(保持堆的结构) |
pop | 删除优先级队列中最大(最小)元素,即堆顶元素 |
top | 返回优先级队列中最大(最小元素),即堆顶元素 |
size | 获取队列中有效元素个数 |
empty | 检测优先级队列是否为空,是返回true,否则返回false |
swap | 交换两个优先级队列的内容 |
priority_queue() | 构造一个空的优先级队列 |
注意:
默认情况下priority_queue是大堆,
#include<functional>
int main()
{
priority_queue<int> q; //默认是vector作为容器适配器,大根堆结构
q.push(3);
q.push(2);
q.push(7);
q.push(0);
while (!q.empty())
{
cout << q.top() << endl; // 7 3 2 0 大根堆
q.pop();
}
cout << endl;
return 0;
}
如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的比较方式的重载,
class Date
{
public:
Date(int year = 1900, int month = 1, 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);
}
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)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
void TestPriorityQueue()
{
// 大堆,需要用户在自定义类型中提供<的重载
priority_queue<Date> q1;
q1.push(Date(2018, 10, 29));
q1.push(Date(2018, 10, 28));
q1.push(Date(2018, 10, 30));
cout << q1.top() << endl;
// 如果要创建小堆,需要用户提供>的重载
priority_queue<Date, vector<Date>, greater<Date>> q2;
q2.push(Date(2018, 10, 29));
q2.push(Date(2018, 10, 28));
q2.push(Date(2018, 10, 30));
cout << q2.top() << endl;
}
当然了,如果自定义类型的类里面不提供的话,我们可以手动实现一个仿函数进行比较
如果要让仿函数类LessPDate能访问Date的私有成员,此时需要定义LessPDate类为Date的友元类
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
//重载<<运算符用于打印, 友元函数
friend ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
_cout << endl;
return _cout;
}
friend struct LessPDate;//定义LessPDate为Date的友元类,让LessPDate也能访问Date的私有成员
private:
int _year;
int _month;
int _day;
};
struct LessPDate
{
bool operator()(const Date* d1, const Date* d2) const
{
return (d1->_year < d2->_year) ||
(d1->_year == d2->_year && d1->_month < d2->_month) ||
(d1->_year == d2->_year && d1->_month == d2->_month && d1->_day < d2->_day);
}
};
void TestPriorityQueue()
{
// priority_queue<Date*> pq; //此时默认是比较地址的大小
//如果需要比较日期的大小,需要自己实现仿函数
priority_queue<Date*, vector<Date*>, LessPDate> pq;
pq.push(new Date(2022, 3, 26));
pq.push(new Date(2021, 10, 26));
pq.push(new Date(2023, 3, 26));
while (!pq.empty())
{
cout << *pq.top();
pq.pop();
}
cout << endl;
}
int main()
{
TestPriorityQueue();
return 0;
}
OJ练习题:
数组中第k大的元素
215. 数组中的第K个最大元素 - 力扣(LeetCode) (leetcode-cn.com)
方法1:使用优先级队列->大根堆, 用数组nums迭代器区间构造优先级队列,然后弹出堆中的前k-1个元素, 此时堆顶的元素就是第k大的元素
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
//默认的仿函数就是less->大根堆
priority_queue<int> q(nums.begin(),nums.end());//用vector的迭代器区间初始化q
for(int i = 0;i<k-1;i++)
{
q.pop();//弹出前k-1个元素
}
return q.top();//返回此时堆顶元素
}
};
时间复杂度:O(N+K*logN) 建N个数的堆 + k个数调堆 空间复杂度:O(N)
方法2:直接排序 sort默认排的是升序, sort的底层是快排 ->时间复杂度:O(N*logN)
升序:下标为 size() - k 的元素就是第k大的元素
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
sort(nums.begin(),nums.end());//升序
return nums[nums.size()-k];
}
};
降序:下标为:k-1就是第k大的元素
传反向迭代器进行排序->就是降序
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
sort(nums.rbegin(),nums.rend());//降序
return nums[k-1];
}
};
仿函数传greater就是降序 需要注意的是:排序函数sort的第三个参数:这里传的是匿名对象!
greater<int>()
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
sort(nums.begin(),nums.end(),greater<int>());//升序
return nums[k-1];
}
};
priority_queue<int, vector<int>, ::greater<int>> q2; ->此时传的是类型,因为它是类模板的参数
而优先级队列的第三个参数需要传类型,为它是类模板的参数
最优解
:方法3:先建一个k个数的**小堆**
后面的元素一次和堆顶元素比较,如果当前元素>堆顶元素,就弹出堆顶元素,然后当前数进堆, 整个数组处理完后,堆顶的元素就是第k大的元素
注意:这里的greater传的是模板参数, 传的是类型!
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
//建一个k个数的小堆->仿函数是greater
//把vector的前k个数放到堆中
priority_queue<int,vector<int>,greater<int>> q(nums.begin(),nums.begin()+k);
//vecotr后面的数依次和堆顶元素比较
for(size_t i = k;i<nums.size();i++)
{
//如果当前数> 堆顶,就替换它,进堆
if(nums[i] > q.top())
{
q.pop();
q.push(nums[i]);
}
}
return q.top();//此时堆顶的元素就是第k大的
}
};
时间复杂度: O(K+(N-K)logK) 空间复杂度:O(K)
- 建k个数的小堆 O(K) + 剩余的N-K个数调堆O(logK *(N-K)), 调一次堆时间复杂度:logK (K为堆的结点个数)
关于仿函数
仿函数比起一般函数具有很多优点:
- 仿函数是模板函数,可以根据不同的类型代表不同的状态
- 在同一时间里,由某个仿函数所代表的单一函数,可能有不同的状态
- 仿函数是模板函数,可以有不同类型
- 仿函数即使定义相同,也可能有不同的类型
- 仿函数在一定程度上使代码更通用,本质上简化了代码
- 仿函数使程序代码变简单
缺点:仿函数是模板函数,其速度比一般函数要慢
在优先级队列中,默认使用less作为仿函数 -> 大堆结构 greater->小堆结构
用一个类重载operator () ,让这个类实例化的对象可以像函数一样使用
例子:
#include<iostream>
using namespace std;
//仿函数 函数对象 这个类的对象可以像函数一样去使用
struct LessInt
{
//比较两个int,重载operator()运算符
bool operator()(int l, int r)
{
return l < r;
}
};
//普通函数
bool LessFun(int l, int r)
{
return l < r;
}
int main()
{
cout << LessFun(1, 2) << endl;
LessInt f1;//使用类创建出的对象
cout << f1(2, 3) << endl;
//会转化成下面的调用:f1(2, 3) ==>f1.operator()(2,3)
//cout<<f1.operator()(2,3)<<endl;
}
为什么要有仿函数 -> 因为函数指针用起来不好用 bool(*f1)(int,int)
仿函数要比较任意类型:->写成模板 写成struct更好,因为struct的默认权限是公有public的
#include<iostream>
using namespace std;
template<class T>
struct Less
{
bool operator()(const T& l, const T& r)
{
return l < r;
}
};
int main()
{
Less<double> less; //给一个模板参数
cout << less(3.3, 4.4) << endl;
}
priority_queue的模拟实现
priority_queue的底层结构就是堆,因此只需对其进行通用的封装即可,
堆:物理上是数组,逻辑上:完全二叉树
需要三个模板参数: 参数类型T
默认容器适配器;vector<T>
仿函数:默认是less<T>
需要实现的接口:
成员函数 | 实现方法 |
---|---|
push | 在容器尾部插入元素后,从最后一个位置进行向上调整 |
pop | 先将容器堆顶元素和最后一个位置的元素交换,再将最后一个元素删除,最后从根结点0位置进行向下调整 |
top | 返回堆顶元素 -> 即返回容器下标为0的位置 |
size | 返回堆中元素个数 -> 即容器的元素个数size() |
empty | 判断堆是否为空->即判断容器是否为空 empty() |
成员变量
//三个模板参数:参数类型T 默认容器适配器;vector<T> 仿函数:默认是less<T> 大堆
template<class T, class Container = vector<T>, class Compare = less<T>>
private:
Container _con;//容器适配器
Compare _cmp;//仿函数
建大堆还是建小堆,本质就是向上调整算法和向下调整算法的比较符号不相同,我们可以通过仿函数来控制
向上调整算法
以大堆为例子
循环结束条件:两个满足其中一个即跳出循环
1.插入的结点的值比从它到根结点的所有结点的值都要大,则要一直向上调整,最坏情况下调整到根结点(堆顶0位置)
2.插入的结点 比 从它到根结点的部分结点的值大,当调整到某一个位置,孩子结点的值比父亲结点的值小,就不用调整了,走else跳出循环
//向上调整算法
void AdjustUp(size_t child)
{
size_t parent = (child - 1) / 2;//算出父节点所在位置
//从孩子位置一直往上调
//结束条件:调整到根节点 / 不满足if条件
while (child > 0)
{
// if(_con[parent]>_con[child])
if (_cmp(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
//继续下一轮向上调整
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
向下调整算法
以大堆为例子, 向下调整的时候, 没有固定的路径可循,和左孩子交换还是和右孩子交换?
由于是大堆, 向下调整,所以和左右孩子中较大的孩子交换
循环结束条件
- 1.最坏情况下:要调整到叶子结点位置(度为0的结点)
- 2.调整到某一个节点的时候, 该结点的值比其孩子中值最大的节点的值大
//向下调整算法
void AdjustDown(size_t parent)
{
//从父节点位置一直往上调
size_t child = parent * 2 + 1;//默认指向左孩子
//结束条件:调整到叶子结点 / 不满足if条件
while (child < _con.size())
{
//大堆:选出较大的孩子,要防止没有右孩子导致越界
if (child + 1 < _con.size() && _cmp(_con[child], _con[child + 1]))
{
++child;//指向右孩子
}
//较大的孩子和父节点比较
//if(_con[parent] > _con[child])
if (_cmp(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
//继续下一轮向下调整
parent = child;//父亲来到孩子位置
child = parent * 2 + 1;//算出此时父亲的孩子的位置
}
else
{
break;
}
}
}
push
先插入一个数据到容器的尾上,再进行向上调整算法,直到满足堆结构
void push(const T& x)
{
_con.push_back(x);//将数据尾插到容器的尾部
AdjustUp(_con.size() - 1);//在插入位置进行向上调整算法
}
pop
将堆顶的数据跟最后一个数据交换,然后删除容器的最后一个数据(即:删除掉了原来的堆顶元素),再从根节点位置进行向下调整算法
void pop()
{
swap(_con[0], _con[_con.size() - 1]);//将容器下标为0的元素和容器下标为size()-1的元素进行交换
_con.pop_back();//然后弹出最后一个元素
AdjustDown(0);//然后从0位置开始向下调整
}
size
获取堆中的元素个数
size_t size()
{
return _con.size();//返回容器中的元素个数
}
top
返回堆顶元素 ,注意:不能返回引用,否则修改之后可能就不符合堆的结构了
T top()
{
return _con[0];//返回容器的第一个元素
}
empty
判断堆是否为空
bool empty()
{
return _con.empty();//返回容器是否为空
}
priority_queue.h代码:
#include<iostream>
using namespace std;
#include<vector>
namespace Mango
{
//仿函数
template<class T>
struct less
{
bool operator()(const T& l, const T& r)
{
return l < r;
}
};
template<class T>
struct greater
{
bool operator()(const T& l, const T& r)
{
return l > r;
}
};
//三个模板参数:参数类型T 默认容器适配器;vector<T> 仿函数:默认是less<T> 默认是大堆
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
public:
//向上调整算法
void AdjustUp(size_t child)
{
size_t parent = (child - 1) / 2;//算出父节点所在位置
//从孩子位置一直往上调
//结束条件:调整到根节点 / 不满足if条件
while (child > 0)
{
// if(_con[parent]>_con[child])
if (_cmp(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
//继续下一轮向上调整
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//向下调整算法
void AdjustDown(size_t parent)
{
//从父节点位置一直往上调
size_t child = parent * 2 + 1;//默认指向左孩子
//结束条件:调整到叶子结点 / 不满足if条件
while (child < _con.size())
{
//选出较大的孩子,要防止没有右孩子导致越界
if (child + 1 < _con.size() && _cmp(_con[child], _con[child + 1]))
{
++child;//指向右孩子
}
//和父节点比较
//if(_con[parent] > _con[child])
if (_cmp(_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);//从根节点向下调整
}
T top() //注意:此处不能返回引用
{
return _con[0];
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;//容器适配器
Compare _cmp;//仿函数
};
}