priority_queue 基本介绍
priority_queue就是优先级队列。其头文件就是queue,但是队列和优先级队列关系不大,两个是不同的数据结构。但二者都是适配器,容器适配器。
优先级队列中存放的数据是有优先级的。
其内部有以下成员方法,其中常用的就empty, size, top, push, pop。
直接演示一下常用方法的使用:
我们看到用法几乎是与栈和队列一样。但是这里打印结果是排好序了(降序)。所以,优先级队列默认情况下是大的优先。
优先级队列的适配器
看其第二个模板参数:class Container = vector,这就是容器适配器。因此我们可以将其底层的容器改为其他的容器(list不行):
第三个模板参数compare
跟sort用法很像,sort第三个参数传的是一个对象,比如说给sort传greater()就是降序,而这里传的是类型,比如说传greater就是小堆。可以看到,模板参数缺省值为less,可能有的同学不知道value_type是啥,其实就是我们日常放在容器中的元素类型,就是那个T,T可以为int、char什么的都行。所以我们默认情况下参数 Compare 就是less的类型,那么就是大堆。
这样就变成了小堆,每次取堆顶的值就是最小值。
- 传greater时是小堆。
- 传less时是大堆。
使用优先级队列来解决这个问题:数组中第K个最大的元素
题目中有要求,必须设计时间复杂度为O(N)的算法。那么先进行排序操作的同学就另寻他路吧。这道题就可以用到优先级队列,就是堆。先把堆建好,然后pop k-1次后的堆顶就是第k大的元素。
class Solution
{
public:
int findKthLargest(vector<int>& nums, int k)
{
priority_queue<int> pq(nums.begin(), nums.end());
while(--k)
{
pq.pop();
}
return pq.top();
}
};
priority_queue 的模拟实现
堆的实现的核心无非就是向上调整和向下调整。而堆虽然逻辑结构上是二叉树,但是实际物理结构就是数组。我们用C++写,默认容器就是vector,因为随机访问数据的次数比较多。我们很多地方就可以直接复用vector中的函数接口,所以就需要自己动手写两个,一个是向上调整,一个是向下调整。
#include<vector>
#include<algorithm>
#include<iostream>
namespace Flash
{
template<class T, class Container = std::vector<T>,class Compare = less<T>>
class priority_queue
{
private:
//向上调整
void adjust_up(size_t child)
{
Compare com;
size_t parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[child] > _con[parent])
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
}
else
{
break;
}
child = parent;
parent = (child - 1) / 2;
}
}
//向下调整
void adjust_down(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 (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
}
else
{
break;
}
parent = child;
child = parent * 2 + 1;
}
}
public:
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
void pop()
{
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
const T& top()
{
return _con[0];
}
bool empty()const
{
return _con.empty();
}
int size()const
{
return _con.size();
}
private:
Container _con;
};
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;
}
};
}
上面基本功能的是实现了,但是问题是不能控制大小堆,那我们可以像库中那样,再搞一个模板参数,传一个仿函数来实现大小堆的控制。
仿函数
仿函数就是一个类,里面重载了()运算符。
第一个ls(1, 2)乍一看就像是函数调用,但实际上就是类匿名对象调用了operator()。
再来个greater:
这就是仿函数,用类来重载()来实现。调用的时候就像函数一样,在C语言阶段学过的qsort,传比较的那个参数的时候要传函数指针,但是函数指针太麻烦了,所以C++为了不再用函数指针,就搞了仿函数。
那么此时我们就可以搞第三个模板参数了。
传的是类型。然后把我们向上调整和向下调整中的代码改一改:
上面的priority_queue、less、greater都是在一个命名空间FangZhang中的,所以除了vector是复用的,剩下的都是手写的,less和greater就是刚写出来的那两个,可以直接用。
再来强调一点:
假如说一个对象vector v,我们用sort时,传参是sort(v.begin(), v.end(), less()),而这堆这是定义对象传模板参数priority_queue<int, vector, less>,前者是传匿名对象,后者是传类型。是不一样的。不要搞混。
栈和队列还有优先级队列都是容器适配器,就是可以改变其底层所使用的容器,从而能够用不同的容器来实现其底层的函数接口。