目录
优先队列(piority queue)是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。优先级队列,是0个或多个元素的集合,每个元素都有一个优先权或值,对优先级队列执行的操作有 1) 查找一个元素;2) 插人一个新元素; 3) 删除一个元素。与这些操作分别对应的函数是top, push 和pop。在最小优先级队列( min piority queue)中,查找和删除的元素都是优先级最小的元素;在最大优先级队列( max priority queue)中,查找和删除的元索都是优先级最大的元素。优先级队列的元素可以有相同的优先级,对这样的元素,查找与删除可以按任意顺序处理。优先级队列类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
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() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
1、默认情况下,priority_queue是大堆。
2、如果在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(2023, 3, 29));
q1.push(Date(2023, 3, 28));
q1.push(Date(2023, 3, 30));
cout << q1.top() << endl;
// 如果要创建小堆,需要用户提供>的重载
priority_queue<Date, vector<Date>, greater<Date>> q2;
q2.push(Date(2023, 3, 29));
q2.push(Date(2023, 3, 28));
q2.push(Date(2023, 3, 30));
cout << q2.top() << endl;
}
仿函数(函数对象)
仿函数的定义
仿函数(Functor)又称为函数对象(Function Object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载 operator() 运算符。因为调用仿函数,实际上就是通过类对象调用重载后的 operator() 运算符。
如果编程者要将某种“操作”当做算法的参数,一般有两种方法:
(1)一个办法就是先将该“操作”设计为一个函数,再将函数指针当做算法的一个参数。
(2)将该“操作”设计为一个仿函数(就语言层面而言是个 class),再以该仿函数产生一个对象,并以此对象作为算法的一个参数。
很明显第二种方法会更优秀,因为第一种方法扩展性较差,当函数参数有所变化,则无法兼容旧的代码。在我们写代码时有时会发现有些功能代码,会不断地被使用。为了复用这些代码,实现为一个公共的函数是一个解决方法。不过函数用到的一些变量,可能是公共的全局变量。引入全局变量,容易出现同名冲突,不方便维护。
使用仿函数了,写一个简单类,除了维护类的基本成员函数外,只需要重载 operator() 运算符 。这样既可以免去对一些公共变量的维护,也可以使重复使用的代码独立出来,以便下次复用。而且相对于函数更优秀的性质,仿函数还可以进行依赖、组合与继承等,这样有利于资源的管理。
在优先级队列中,默认是大根堆,我们如果需要小根堆就可以使用仿函数。在C++中,仿函数一般不需要我们自己去写,库函数中有。需要包含头文件 #include <functional>。
int findKthLargest(vector<int>& nums, int k) {
//优先级队列默认大堆,使用仿函数建立小堆
priority_queue<int,vector<int>,greater<int>> pq(nums.begin(),nums.begin()+k);
for(int i=k;i<nums.size();i++)
{
if(nums[i]>pq.top())
{
pq.pop();
pq.push(nums[i]);
}
}
return pq.top();
}
特殊的仿函数需要自己实现,比如地址的比较。
class PDateLess { public: bool operator()(const Date* p1, const Date* p2) { return *p1 < *p2; } }; class PDateGreater { public: bool operator()(const Date* p1, const Date* p2) { return *p1 > *p2; } };int main()
{
priority_queue<Date*, vector<Date*>, PDateGreater> q;
q.push(new Date(2023, 10, 29));
q.push(new Date(2023, 10, 30));
q.push(new Date(2023, 10, 28));
cout << *(q2.top()) << endl;
}
优先级队列常见经典OJ题(面试常考)
剑指 Offer 41. 数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
- void addNum(int num) - 从数据流中添加一个整数到数据结构中。
- double findMedian() - 返回目前所有元素的中位数。
大顶堆存放的是不比中位数大的数 也就是说存放的是有序数组中左半部分
小顶堆存放的是比中位数大的数 也就是说存放的是有序数组中右半部分
大顶堆的栈顶表示的是左半部分最大的数
小顶堆的栈顶表示的是右半部分最小的数
同时这两个堆存放的元素数量相差不会超过1
当两个堆存放总数为奇数时,大顶堆(即左半部分堆)的栈顶就是中位数
当两个堆存放总数为偶数时,结合大顶堆栈顶和小顶堆栈顶可求出中位数
接下来考虑插入问题 即当新元素进来时,应该怎么插入数据?
若两个堆的数据量为偶数时,那么就先插入到小顶堆找到最小值(堆顶元素),
随后把这个最小值放入到大顶堆(左半部分), 此时两个堆的数据量为奇数,中位数为大顶堆堆顶
若两个堆的数据量为奇数时,那么就先插入到大顶堆找最大值(堆顶元素),
随后把这个最大值放入到小顶堆(右半部分),此时两个堆的数据量为偶数,中位数通过两个堆堆顶求出。
class MedianFinder {
public:
// 最大堆,存储左边一半的数据,堆顶为最大值
priority_queue<int, vector<int>, less<int>> maxHeap;
// 最小堆, 存储右边一半的数据,堆顶为最小值
priority_queue<int, vector<int>, greater<int>> minHeap;
/** initialize your data structure here. */
MedianFinder() {
}
// 维持堆数据平衡,并保证左边堆的最大值小于或等于右边堆的最小值
void addNum(int num) {
/*
* 当两堆的数据个数相等时候,左边堆添加元素。
* 采用的方法不是直接将数据插入左边堆,而是将数据先插入右边堆,算法调整后
* 将堆顶的数据插入到左边堆,这样保证左边堆插入的元素始终是右边堆的最小值。
* 同理左边数据多,往右边堆添加数据的时候,先将数据放入左边堆,选出最大值放到右边堆中。
*/
if (maxHeap.size() == minHeap.size()) {
minHeap.push(num);
int top = minHeap.top();
minHeap.pop();
maxHeap.push(top);
} else {
maxHeap.push(num);
int top = maxHeap.top();
maxHeap.pop();
minHeap.push(top);
}
}
double findMedian() {
if (maxHeap.size() == minHeap.size()) {
return (maxHeap.top()+minHeap.top())*1.0/2;
} else {
return maxHeap.top()*1.0;
}
}
};
把数组排成最小的数
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
此题求拼接起来的最小数字,本质上是一个排序问题。设数组 nums 中任意两数字的字符串为 x 和 y ,则规定 排序判断规则为:
若拼接字符串 x+y>y+x,则 x “大于” y ;
反之,若 x+y<y+x,则 x“小于” y ;
x “小于” y 代表:排序完成后,数组中 x 应在 y 左边;“大于” 则反之。
class Solution {
public:
//仿函数
bool operator()(string s1,string s2)
{
return s1+s2 < s2+s1;
}
string minNumber(vector<int>& nums)
{
vector<string> a;
for(int i=0;i<nums.size();i++)
{
a.push_back(to_string(nums[i]));
}
sort(a.begin(),a.end(),Solution());//仿函数的运用
string ret;
for(int i=0;i<a.size();i++)
{
ret += a[i];
}
return ret;
}
};