优先级队列
1、介绍
priority_queue
是 C++ STL 中的一个标准容器,它基于堆的数据结构实现,可以方便地实现堆的基本操作,例如插入元素、弹出最大或最小元素等。
其模板参数为:
template <class T, class Container = vector<T>,
class Compare = less<typename Container::value_type> >
priority_queue
的模板参数包括三个:
class T
:表示存储在priority_queue
中的元素类型。可以是任何可比较的类型,例如基本数据类型(如int
、double
)、结构体、类等。class Container = vector<T>
:表示底层容器的类型,缺省情况下采用的是vector<T>
,也可以自定义容器类型,只要该容器支持随机访问、大小可变、在末尾快速插入元素以及在任意位置快速删除元素即可。class Compare = less<typename Container::value_type>
:表示元素之间的比较函数对象的类型,缺省情况下采用的是less<typename Container::value_type>
,即使用<
运算符来比较元素的大小关系。如果需要使用自定义的比较函数,可以通过这个模板参数来指定。这个函数对象应该接受两个参数,返回一个bool
类型的值,表示它们之间的大小关系。
int main()
{
priority_queue<int> pq;
pq.push(3);
pq.push(2);
pq.push(5);
pq.push(1);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
return 0;
}
可见,默认(缺省)情况下,采用的是大数优先(大堆)(less),我们也可更改比较方式,形成其它的优先情况:
例如,可以定义一个存储 int
类型元素、采用 vector<int>
作为底层容器、采用 greater<int>
函数对象作为比较函数的 priority_queue
对象:
#include <queue>
#include <functional> //使用greater的头文件
using namespace std;
int main() {
priority_queue<int, vector<int>, greater<int>> pq;
return 0;
}
对上述push的3-2-5-1结果如下:(小的优先级高)(greater)
2、应用
我们通过力扣例题的方式来体会该容器的应用
思路:
对于本题来说,我们直观的思路即是排序;其次,我们在介绍学习数据结构时通过TOP-K的方法建立大堆或小堆也可以解决本题:
①建立n个数的大堆(时间复杂度为O(n)),堆顶的数时最大的数,因此找到第K大的只用pop()K次堆顶
,每次pop()之后进行向下调整,但面对n很大K很小的情况时对资源的开销较大
②建立K个数的小堆,然后对剩下n-K个数依次和堆顶的数比较,若大于堆顶则替换掉原来的堆顶并进堆,再向上调整形成新的堆,重复执行以上操作,最终堆顶的数即是第K大的数,时间复杂度为O(K+(N-K)*logK)
从算法的时间复杂度来说,两者差异都不大;由于priority_queue
容器默认是建大堆,因此本题我们采用建n个数的大堆的方式实现
需要注意的是,以上所谓的向上调整和向下调整是针对于C语言实现的数据结构而言的,我们使用
priority_queue
容器是根据我们的比较方式自动建立大堆或小堆**(即自动调整)**
题解:
class Solution
{
public:
int findKthLargest(vector<int>& nums, int k)
{
priority_queue<int> pq(nums.begin(),nums.end());
//走k-1次
while(--k)
{
pq.pop();
}
return pq.top();
}
};
我们同时也可以试一试用建K个数的小堆的方法:
class Solution
{
public:
int findKthLargest(vector<int>& nums, int k)
{
//建立K个数的小堆
//注意不要再用缺省模板参数构造
priority_queue<int,vector<int>,greater<int>> pq(nums.begin(),nums.begin()+k);
for(size_t i=k;i<nums.size();i++)
{
if(nums[i]>pq.top())
{
pq.pop();
pq.push(nums[i]);
}
}
return pq.top();
}
};
3、优先级队列的底层及模拟实现
优先级队列(Priority Queue)它是一种常见的数据结构,通常用堆(Heap)来实现。堆是一种完全二叉树,具有以下两个性质:
- 父节点的值始终小于或等于子节点的值(最小堆),或者父节点的值始终大于或等于子节点的值(最大堆)。
- 对于同一深度的节点,从左到右排列,不存在空的位置。
在优先级队列中,每个元素都有一个优先级值,根据这个值决定元素在队列中的位置。最小堆中,优先级值越小的元素越靠近堆的根部,也就是队首;最大堆中,优先级值越大的元素越靠近堆的根部。
堆的底层实现通常使用数组来表示完全二叉树,每个节点在数组中的位置可以通过计算得到。堆中插入元素的操作可以先将新元素插入到数组末尾,然后不断将它与其父节点比较交换,直到满足堆的性质。删除堆顶元素的操作可以将堆顶元素与数组末尾的元素交换,然后将堆顶元素不断与其子节点比较交换,直到满足堆的性质。
模拟实现如下:
#include <iostream>
#include <vector>
using namespace std;
template<typename T, typename Container = std::vector<T>>
class PriorityQueue
{
public:
//构造函数
PriorityQueue() {}
//插入
void push(const T& val)
{
data.push_back(val);
adjust_up(data.size() - 1);
}
//取队顶
T top() const
{
return data.front();
}
//出队
void pop()
{
if (data.empty())
{
return;
}
swap(data[0], data.back());
data.pop_back();
adjust_down(0);
}
//队列长度
size_t size() const
{
return data.size();
}
//判断队列是否为空
bool empty() const
{
return data.empty();
}
private:
Container data;
//向上调整
void adjust_up(size_t child)
{
while (child > 0)
{
size_t parent = (child - 1) / 2;
if (data[child] > data[parent])
{
swap(data[child], data[parent]);
child = parent;
}
else
{
break;
}
}
}
//向下调整
void adjust_down(size_t parent)
{
size_t child = parent * 2 + 1;
while (child < data.size())
{
// 将 child 赋值为左右孩子中的最大值
if (child + 1 < data.size() && data[child + 1] > data[child])
{
child++;
}
// 如果最大子节点比当前节点更大,就将它们交换,并继续向下调整
if (data[child] > data[parent])
{
swap(data[child], data[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
};
4、仿函数
在 C++ 中,仿函数(Functor)是一个类或者结构体,它实现了一个函数调用操作符operator()
,使得对象可以像函数一样被调用**(括号操作符重载)**
一个仿函数的基本实现如下:
//模板参数
template<typename T>
//Functor为仿函数名
class Functor {
public:
void operator()(T arg) const
{
// do something with arg
}
};
下面是一个简单的仿函数,用于比较两个字符串的长度:
class StringLengthCompare {
public:
bool operator()(const std::string& str1, const std::string& str2) const
{
return str1.length() < str2.length();
}
};
该仿函数接受两个字符串参数,并返回一个布尔值,指示哪个字符串更短
我们可以使用仿函数的 less
和 greater
来定义优先级队列的比较方式;在 C++ STL 中,优先级队列的默认比较方式是 less,即小顶堆,但我们也可以通过定义一个仿函数,然后将其作为模板参数传递给优先级队列来修改比较方式
其中仿函数的 less
和 greater
实现如下:
// less 仿函数
template<typename T>
struct less {
bool operator() (const T& left, const T& right) const {
return left < right;
}
};
// greater 仿函数
template<typename T>
struct greater {
bool operator() (const T& lhs, const T& rhs) const {
return left > right;
}
};
用在优先级队列如下:
// 使用 less 仿函数定义小顶堆
priority_queue<int, vector<int>, less<int>> min_heap;
// 使用 greater 仿函数定义大顶堆
priority_queue<int, vector<int>, greater<int>> max_heap;
而比较方式主要用于建立大堆还是小堆,因此在优先级队列模拟实现中,表现在向上向下调整算法中:
// 使用 greater 仿函数定义大顶堆
template<typename T>
void adjustUp(vector<T>& heap, int i)
{
int parent = (i - 1) / 2;
while (i > 0 && greater<T>()(heap[parent], heap[i]))
{
swap(heap[parent], heap[i]);
i = parent;
parent = (i - 1) / 2;
}
}
// 使用 less 仿函数定义小顶堆
template<typename T>
void adjustDown(vector<T>& heap, int i, int n)
{
int leftChild = 2 * i + 1;
int rightChild = 2 * i + 2;
int maxIdx = i;
if (leftChild < n && less<T>()(heap[maxIdx], heap[leftChild]))
{
maxIdx = leftChild;
}
if (rightChild < n && less<T>()(heap[maxIdx], heap[rightChild]))
{
maxIdx = rightChild;
}
if (maxIdx != i)
{
swap(heap[i], heap[maxIdx]);
adjustDown(heap, maxIdx, n);
}
}
在使用仿函数时,我们通过 less<T>()
和 greater<T>()
的方式来创建一个 less 或者 greater 类型的仿函数,并将其传递给需要使用比较器的函数或者容器。