前言
- priority_queue常用接口的使用
- priority_queue的模拟实现
- 了解仿函数
一、priority_queue的介绍和使用
1. priority_queue的介绍
- 优先级队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
- 文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元
素)。- 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的"尾部"弹出,其称为优先队列的顶部。
- 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
- empty():检测容器是否为空
- size():返回容器中有效元素个数
- front():返回容器中第一个元素的引用
- push_back():在容器尾部插入元素
- pop_back():删除容器尾部元素
- 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
- 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
2. priority_queue的使用
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。
常用的成员函数如下:
函数声明 接口说明 priority_queue() / priority_queue(InputIterator first, InputIterator last) 构造一个空的优先级队列 使用迭代器区间[first,last)构造一个优先级队列 empty() 检测优先级队列是否为空,是返回true,否则返回false top() 返回优先级队列中最大(最小)元素,即堆顶元素 push(val) 在优先级队列中插入元素val pop() 删除优先级队列中最大(最小)元素,即堆顶元素 tip:priority_pueue的底层容器默认使用vector,通过仿函数来控制实现大小堆,默认是大堆。
priority_queue常用接口使用的代码示例:
cpp
#include<iostream>
#include<queue>
using namespace std;
void test_priority_queue()
{
//构造一个空的优先级队列,默认是大堆
priority_queue<int> pq;
//向优先级队列中插入元素
pq.push(3);
pq.push(5);
pq.push(1);
pq.push(4);
//与栈和队列一样,优先级队列也是不支持遍历的,没有迭代器
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
//通过仿函数控制实现小堆
priority_queue<int, vector<int>, greater<int>> pq1;
//向优先级队列中插入元素
pq1.push(3);
pq1.push(5);
pq1.push(1);
pq1.push(4);
//与栈和队列一样,优先级队列也是不支持遍历的,没有迭代器
while (!pq1.empty())
{
cout << pq1.top() << " ";
pq1.pop();
}
cout << endl;
}
priority_queue在OJ中的使用:数组中的第k个最大元素
思路1:数据量小,O(N*logN)和O(N)是差不多的,所以可以直接使用sort
思路2:建大堆,pop k-1 次,最后的堆顶元素就是第k个最大元素,时间复杂度O(N + K * logN)
思路3:方式3:建小堆,先使用数组前k个元素建小堆,再for遍历数组后面的元素,如果数组元素大于堆顶元素就先pop堆顶元素再push数组元素,最后堆顶元素就是第k个最大元素
代码示例:
cpp
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
//方式1:数据量小,直接sort排降序,返回第K大元素
//sort(nums.begin(), nums.end(), greater<int>());
//return nums[k-1];
//方式2:建大堆,pop k-1 次,最后返回的堆顶元素就是第k个最大元素
//N
//priority_queue<int> pq(nums.begin(), nums.end());
//K*log(N)
//while(--k)
//{
// pq.pop();
//}
//return pq.top();
//方式3:建小堆,先使用数组前k个元素建小堆,再for遍历数组后面的元素,如果数组元素大于堆顶元素就先pop堆顶元素再push数组元素,最后堆顶元素就是第k个最大元素
//K
priority_queue<int, vector<int>, greater<int>> pq(nums.begin(), nums.begin() + k);
//(N - K) * logK
for(int i = k; i < nums.size(); ++i)
{
if(nums[i] > pq.top())
{
pq.pop();
pq.push(nums[i]);
}
}
return pq.top();
}
};
tip:sort是函数模版里面要传对象所以greater要带()表示是一个匿名对象,类模板里面要传类型所以greater不带()
二、priority_queue的模拟
1. 不使用仿函数模拟实现
- 为了避免与库中的priority_queue发生冲突,使用命名空间隔离
- 模拟实现优先级队列,底层容器默认vector,且默认大堆
- 建堆:向下调整建堆
- 前提:左右子树都是堆,所以我们需要从倒数第一个非叶子结点开始,倒数第一个非叶子结点(n - 1 - 1)/ 2,因为数组下标从0开始,所以我们需要多减一个1。
- 假设法选出左右孩子中较大的孩子结点:先假设左孩子较大,再验证。注意右孩子是否存在。
- 将较大的那个孩子与其父节点比较,如果父节点小,则交换,如果父节点大,则break。
- 最坏情况:父节点一直比孩子小,一直向下调整交换到叶子才结束。
- 删除堆顶元素:
- 一交换:把堆顶元素与最后一个元素交换
- 二尾删:数组尾删,删除堆顶元素
- 三堆顶元素向下调整
- 优先级队列插入元素:
- 一尾插:先将插入元素尾插到数组尾
- 二向上调整
- 向上调整:
- 将目标节点与其父节点比较,如果目标节点大于其父节点则交换,如果不大于,则break。
- 最坏情况:目标节点一直大于其父节点,一直向上调整交换到根才结束。
- 注意:不能使用父节点作为结束条件,因为parent =(child-1)/ 2 不可能小于0,所以要使用child > 0最为结束条件。
cpp
namespace wjs
{
//模拟优先级队列:默认大堆
template<class T, class Container = vector<T>>
class priority_queue
{
private:
//堆的向下调整算法
void AdjustDown(int parent)
{
//找出左右孩子大的那个
//假设法:先假设左孩子大,再验证是否假设成立。注意:右孩子是否存在
int child = parent * 2 + 1;
//向下调整,直到父节点大于孩子结点或最坏情况到叶子结点结束
while (child < _con.size())
{
if (child + 1 < _con.size() && _con[child + 1] > _con[child])
{
//右孩子存在,且右孩子大于左孩子,假设不成立,让其指向右孩子
++child;
}
//父节点与大的孩子进行比较,如果父节点小,则交换
if (_con[child] > _con[parent])
{
swap(_con[child], _con[parent]);
//迭代:继续向下比较
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆的向上调整算法
void AdjustUp(int child)
{
int parent = (child - 1) / 2;
//向上调整:最坏情况到根结点才结束
//注意:不能使用parent < 0来判断是否到跟,因为parent = (child - 1) / 2不可能小于0
while (child > 0)
{
//如果目标节点大于其父节点,交换
if (_con[child] > _con[parent])
{
swap(_con[child], _con[parent]);
//迭代:继续向上调整
child = parent;
parent = (child - 1) / 2;
}
//如果不大于,结束向上调整
else
{
break;
}
}
}
public:
//默认构造------调用自定义类型的默认构造
priority_queue()
{}
//使用迭代器区间构造优先级队列
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{
//先把所有数据插入进去
while (first != last)
{
_con.push_back(*first);
++first;
}
//建堆:向下调整建堆
//前提:左右子树是堆,所以从倒数第一个叶子结点的父亲开始
for (int i = ((int)_con.size() - 2) / 2; i >= 0; i--)
{
//向下调整
AdjustDown(i);
}
}
//删除堆顶元素
void pop()
{
//1.交换:把堆顶元素与最后一个元素交换
swap(_con[0], _con[_con.size() - 1]);
//2.删除堆顶元素,即数组尾删
_con.pop_back();
//3.向下调整
AdjustDown(0);
}
//插入元素
void push(const T& x)
{
//1.先将插入元素尾插到数组尾
_con.push_back(x);
//2.向上调整
AdjustUp(_con.size() - 1);
}
//取堆顶元素的引用
const T& top()
{
return _con[0];
}
//判断优先级队列是否为空
bool empty()
{
//为空返回true,反之不为空返回false
return _con.empty();
}
//返回优先级队列的有效元素个数
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
2. 仿函数
- 仿函数:仿函数(functor),就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。
- 仿函数(更多指的是这个类)/函数对象(更多指这个类定义的对象)
- 仿函数简单理解就是这个类对象可以像函数一样使用
- 意义:
- 在堆的向上调整和向下调整中控制大小堆直接比较就写死了,使用函数指针或仿函数可以灵活的比较,但是函数指针可读性差且易出错,所以C++就不想使用函数指针,使用仿函数,像对象一样的使用
- 比较的类型不是我们想要的类型,我们可以自己实现仿函数来比较
cpp
#pragma once
namespace wjs
{
//仿函数(更多指的是这个类)/函数对象(更多指这个类定义的对象)
template<class T>
class less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
class greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
}
int main()
{
//仿函数/函数对象
//仿函数就是这个类对象可以像函数一样使用
//意义:直接比较就写死了,使用函数指针或仿函数,函数指针可读性差且易出错,所以C++就不想使用函数指针,使用仿函数,像对象一样的使用
wjs::less<int> lessfunc;
cout << lessfunc(3, 4) << endl;
cout << lessfunc.operator()(3, 4) << endl;
return 0;
}
3. 使用仿函数模拟实现
- 大小堆的控制只需要控制向上调整算法与向下调整算法中父节点与目标节点的大小比较。我们可以添加一个模版参数为仿函数,自己决定大小堆。
- 就是通过模版参数来控制仿函数的类型,通过仿函数来控制比较。
cpp
#pragma once
namespace wjs
{
//仿函数(更多指的是这个类)/函数对象(更多指这个类定义的对象)
template<class T>
class less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
class greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
//模拟优先级队列:默认大堆
template<class T, class Container = vector<T>, class Comapre = less<T>>
class priority_queue
{
private:
//堆的向下调整算法
void AdjustDown(int parent)
{
Comapre com;
//找出左右孩子大的那个
//假设法:先假设左孩子大,再验证是否假设成立。注意:右孩子是否存在
int child = parent * 2 + 1;
//向下调整,直到父节点大于孩子结点或最坏情况到叶子结点结束
while (child < _con.size())
{
//if (child + 1 < _con.size() && _con[child + 1] > _con[child])
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
//右孩子存在,且右孩子大于左孩子,假设不成立,让其指向右孩子
++child;
}
//父节点与大的孩子进行比较,如果父节点小,则交换
//if(_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
//迭代:继续向下比较
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆的向上调整算法
void AdjustUp(int child)
{
Comapre com;
int parent = (child - 1) / 2;
//向上调整:最坏情况到根结点才结束
//注意:不能使用parent < 0来判断是否到跟,因为parent = (child - 1) / 2不可能小于0
while (child > 0)
{
//如果目标节点大于其父节点,交换
//if (_con[child] > _con[parent])
if(com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
//迭代:继续向上调整
child = parent;
parent = (child - 1) / 2;
}
//如果不大于,结束向上调整
else
{
break;
}
}
}
public:
//默认构造------调用自定义类型的默认构造
priority_queue()
{}
//使用迭代器区间构造优先级队列
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{
//先把所有数据插入进去
while (first != last)
{
_con.push_back(*first);
++first;
}
//建堆:向下调整建堆
//前提:左右子树是堆,所以从倒数第一个叶子结点的父亲开始
for (int i = ((int)_con.size() - 2) / 2; i >= 0; i--)
{
//向下调整
AdjustDown(i);
}
}
//删除堆顶元素
void pop()
{
//1.交换:把堆顶元素与最后一个元素交换
swap(_con[0], _con[_con.size() - 1]);
//2.删除堆顶元素,即数组尾删
_con.pop_back();
//3.向下调整
AdjustDown(0);
}
//插入元素
void push(const T& x)
{
//1.先将插入元素尾插到数组尾
_con.push_back(x);
//2.向上调整
AdjustUp((int)_con.size() - 1);
}
//取堆顶元素的引用
const T& top()
{
return _con[0];
}
//判断优先级队列是否为空
bool empty()
{
//为空返回true,反之不为空返回false
return _con.empty();
}
//返回优先级队列的有效元素个数
size_t size()
{
return _con.size();
}
private:
Container _con;
};
//测试代码
void test_priority_queue()
{
//构造一个空的优先级队列,默认是大堆
priority_queue<int> pq;
//向优先级队列中插入元素
pq.push(3);
pq.push(5);
pq.push(1);
pq.push(4);
//与栈和队列一样,优先级队列也是不支持遍历的,没有迭代器
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
//通过仿函数控制实现小堆
priority_queue<int, vector<int>, greater<int>> pq1;
//向优先级队列中插入元素
pq1.push(3);
pq1.push(5);
pq1.push(1);
pq1.push(4);
//与栈和队列一样,优先级队列也是不支持遍历的,没有迭代器
while (!pq1.empty())
{
cout << pq1.top() << " ";
pq1.pop();
}
cout << endl;
}
}