前言
不久前记录了单调队列的用法,而优先队列则是和单调队列很相似的一种数据结构。
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。
在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出的行为特征。在优先队列里,元素被储存在“堆里面”,里面的元素是大致有序的(不是严格有序的),但是每次pop出来的元素,都是最大(或者最小)的元素,然后pop出去之后,再重新调整堆内的元素,使最大(最小)的元素处于最顶端。
C++中优先队列的实现
和队列基本操作相同:
top 访问队头元素
empty 队列是否为空
size 返回队列内元素个数
push 插入元素到队尾 (并排序)
emplace 原地构造一个元素并插入队列
pop 弹出队头元素
swap 交换内容
定义:priority_queue<Type, Container, Functional>
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式。
当需要用自定义的数据类型时才需要传入这三个参数;
//升序队列
priority_queue <int,vector<int>,greater<int> > q; //队头元素是所有元素中的最小值
//降序队列
priority_queue <int,vector<int>,less<int> >q; //队头元素是所有元素中的最大值
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
什么是大顶堆和小顶堆
一、什么是堆
堆是一种非线性结构,可以把堆看作一棵二叉树,也可以看作一个数组,即:堆就是利用完全二叉树的结构来维护的一维数组。
堆可以分为大顶堆和小顶堆。
大顶堆:每个结点的值都大于或等于其左右孩子结点的值。
小顶堆:每个结点的值都小于或等于其左右孩子结点的值。
如果是排序,求升序用大顶堆,求降序用小顶堆。
一般我们说 topK
问题,就可以用大顶堆或小顶堆来实现,
最大的 K 个:小顶堆
最小的 K 个:大顶堆
二、大顶堆的构建过程
大顶堆的构建过程就是从最后一个非叶子结点开始从下往上调整。
最后一个非叶子节点怎么找?这里我们用数组表示待排序序列,则最后一个非叶子结点的位置是:数组长度/2-1。假如数组长度为9,则最后一个非叶子结点位置是 9/2-1=3。
比较当前结点的值和左子树的值,如果当前节点小于左子树的值,就交换当前节点和左子树;
交换完后要检查左子树是否满足大顶堆的性质,不满足则重新调整子树结构;
再比较当前结点的值和右子树的值,如果当前节点小于右子树的值,就交换当前节点和右子树;
交换完后要检查右子树是否满足大顶堆的性质,不满足则重新调整子树结构;
无需交换调整的时候,则大顶堆构建完成。
在C++优先队列里面,对于基础类型默认是大顶堆,比如priority_queue< int > a,等同于 priority_queue<int, vector< int >, less< int > > a; 这里一定要有空格,不然成了右移运算符
priority_queue<int, vector< int >, greater< int > > c这样就是小顶堆。
下面是用pair做优先队列元素的例子:
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int main()
{
priority_queue<pair<int, int> > a;
pair<int, int> b(1, 2);
pair<int, int> c(1, 3);
pair<int, int> d(2, 5);
a.push(d);
a.push(c);
a.push(b);
while (!a.empty())
{
cout << a.top().first << ' ' << a.top().second << '\n';
a.pop();
}
}
输出是:
下面是用自定义数据类型做优先队列元素的例子:
经典题目
一、前k个高频元素
题目链接:347. 前 K 个高频元素 - 力扣(LeetCode)
题目要求:给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
C++ 代码:
class Solution {
public:
class myCmp{
public://这里自己定义的仿函数一定要是public的,不能是private的
bool operator()(const pair<int,int>& a,const pair<int,int>& b)
{
return a.second > b.second;//注意,这里和快排里面自己定义的cmp函数不一样,如果是 >,就是小根堆,反之就是大根堆
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
//用c++自带的stl里的优先队列来做
priority_queue<pair<int,int>,vector<pair<int,int>>,myCmp> pri_que;//注意定义的方式
unordered_map<int,int>record;
for(auto i:nums)
record[i] ++;
for(auto it = record.begin();it != record.end();it ++)
{
pri_que.push(*it);
if(pri_que.size() > k)//用的是小顶堆,只维护k个数,每次把最小的数都pop出去,最后剩下的就是最大的k个数
pri_que.pop();
}
vector<int>ans;
while(k)
{
ans.push_back(pri_que.top().first);
pri_que.pop();
k --;
}
return ans;
}
};
二、丑数
题目链接:264. 丑数 II - 力扣(LeetCode)
题目要求:给你一个整数 n
,请你找出并返回第 n
个 丑数 。
丑数 就是只包含质因数 2
、3
和/或 5
的正整数。
思路:首先,将最小的丑数1入堆,每次pop出最小的丑数x,而且2x,3x,5x也都是丑数,分别都压入堆中 。如此重复n次,第n个pop出来的元素就是第n个丑数。
class Solution {
public:
int nthUglyNumber(int n) {
//方法一,优先队列
//首先,将最小的丑数1入堆
//每次pop出最小的丑数x,而且2x,3x,5x也都是丑数,分别都压入堆中
//如此重复n次,第n个pop出来的元素就是第n个丑数
//为了避免同一个元素重复入队,用set来记录入队过的元素
priority_queue<long,vector<long>,greater<long> >que;
que.push(1);
unordered_set<long>record;
long ans = 0;
while(n)
{
ans = que.top();
que.pop();
n --;
if(record.find(2*ans) == record.end())
{
que.push(2*ans);
record.insert(2*ans);
}
if(record.find(3*ans) == record.end())
{
que.push(3*ans);
record.insert(3*ans);
}
if(record.find(5*ans) == record.end())
{
que.push(5*ans);
record.insert(5*ans);
}
}
return ans;
}
};