✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C++学习
贝蒂的主页:Betty’s blog
1. priority_queue的介绍
1.1. priority_queue的概念
priority_queue
即为**优先级队列,**它是STL对数据结构中堆的具体封装,因为priority_queue
的头文件倍包含在queue
的头文件中,所以使用时只需要包含头文件#include<queue>
,并且使用优先级队列建堆,默认为大堆。
1.2. priority_queue的特点
priority_queue
的模版参数有三个,第一个T
是我们的元素类型,第二个Container
是我们的容器适配器,第三个Compare
是我们的仿函数。
其中需要注意的是:
priority_queue
的容器适配器需要具有empty()
,size()
,front()
,push_back()
,pop_back()
五种功能,其中常见的容器有vector
与deque
。priority_queue
默认选择的就是vector
。priority_queue
的容器适配器需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap
、push_heap
和pop_heap
来自动完成此操作。
2. 仿函数
2.1. 仿函数的概念
接下来我们来重点介绍一下priority_queue
的三个模版参数Compare
——仿函数。
**仿函数(Functor)**是一种行为类似函数的对象,它可以被用作函数并接受参数。在 C++ 中,仿函数通常是重载了函数调用运算符 operator()
的类对象。通过重载 operator()
,仿函数可以像函数一样被调用,并且可以保存状态信息。
下面是一个具体的仿函数例子:
// 定义一个比较小于的仿函数
template<class T>
struct Less
{
//重载函数调用运算符
bool operator()(const T&a, const T&b)
{
return a < b;
}
};
int main()
{
Less<int> le;//定义一个仿函数对象
int a = 5, b = 9;
cout << le(a, b) << endl;//采用仿函数比较
cout << (Less<int>().operator()(a,b)) << endl;//完整调用
return 0;
}
然后我们可以通过定义对象的方式调用,也可以通过完整的类调用函数方式调用。
2.2. 仿函数的应用
在C++的标准库中,仿函数的使用就较为普遍,一般都作为模版参数进行传参。比如说算法库algorithm
中的排序算法sort
,默认排序就是升序,如果需要降序排序就需要传仿函数或函数调整。这个仿函数或者函数我们可以自己定义,也可以借用标准库中提供的函数。其中比较常用的就是比较小于的less
,比较大于的greater
。使用时需要包含头文件#include<functional>
。
void Test1()
{
vector<int> arr1 = { 5,4,6,7,2,9,10,8 };
vector<int> arr2 = arr1;
sort(arr1.begin(), arr1.end());//默认为升序
for (auto& e : arr1)
{
cout << e << " ";
}
cout << endl;
//通过匿名函数对象greater<int>()调整为升序
sort(arr2.begin(), arr2.end(),greater<int>());
for (auto& e : arr2)
{
cout << e << " ";
}
cout << endl;
}
2.3. 仿函数与函数指针的对比
其实仿函数的用法与我们C语言中的函数指针非常类似,或者说就是为了简化C语言的函数指针,C++才引入了仿函数这个概念。
以下是仿函数与函数指针的对比:
灵活性:
- 仿函数可以保存状态信息,因为它是一个类对象,可以有成员变量来存储相关数据。例如,一个计算累加和的仿函数可以保存当前的总和。
- 函数指针只是指向一个函数,无法保存额外的状态。
封装性:
- 仿函数可以将相关的操作和数据封装在一个类中,提供更好的封装性和代码组织性。
- 函数指针本身不具备封装特性。
代码可读性:
- 仿函数的使用在某些情况下可以使代码更具可读性,特别是当相关的操作较为复杂且需要维护状态时。
- 函数指针的使用可能会使代码看起来较为简洁,但对于复杂逻辑可能不太直观。
可扩展性:
- 仿函数可以通过继承和多态性进行扩展和定制。
- 函数指针在这方面的扩展性较差。
3. priority_queue的用法
priority_queue
提供的接口相比较与其他容器也明显较少,这是由于其结构决定的,并且由于priority_queue
也并不需要遍历,所以也不存在迭代器的概念。
以下就是priority_queue
的接口:
并且由于我们学习过数据结构中的堆,所以使用这里的接口的成本也比较低。
void Test2()
{
priority_queue<int> heap;//默认为大堆
heap.push(1);
heap.push(2);
heap.push(3);
heap.push(4);
heap.push(5);
heap.push(6);
cout << "堆的大小为:" << heap.size() << endl;
cout << "堆顶元素为:" << heap.top() << endl;//最大值
while (!heap.empty())
{
int top = heap.top();
cout << top << " ";
heap.pop();
}
cout << endl;
}
当然我们也可以使用greater
仿函数让其变为小堆。
void Test3()
{
priority_queue<int,vector<int>,greater<int>> heap;//小堆
heap.push(1);
heap.push(2);
heap.push(3);
heap.push(4);
heap.push(5);
heap.push(6);
cout << "堆的大小为:" << heap.size() << endl;
cout << "堆顶元素为:" << heap.top() << endl;//最小值
while (!heap.empty())
{
int top = heap.top();
cout << top << " ";
heap.pop();
}
cout << endl;
}
4. 模拟实现priority_queue
因为我们学习过堆这个数据结构,所以封装priority_queue
会非常简单,只需要在原有的基础上增加一个仿函数即可。
4.1. 插入
向堆的末尾插入新的元素,然后与父节点比较,然后判断是否需要向上调整。假设插入元素23,如下图:
//向上调整
void adjust_up(size_t child)
{
Compare com;//使用仿函数
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void push(const T& val)
{
_con.push_back(val);
adjust_up(_con.size() - 1);
}
4.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 (com(_con[parent], _con[child]))
{
std::swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void pop()
{
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
}
4.3. 其他接口
剩余的size()
,empty()
,top()
等接口实现就非常容易了,这需要复用我们容器适配器的接口即可。
const T& top()const
{
return _con[0];
}
size_t size()const
{
return _con.size();
}
bool empty()const
{
return _con.empty();
}
5. 源码
#include<vector>
#include<functional>
namespace betty
{
template<class T,class Container=vector<T>,class Compare=less<T>>
class priority_queue
{
public:
//向上调整
void adjust_up(size_t child)
{
Compare com;//使用仿函数
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//向下调整
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 (com(_con[parent], _con[child]))
{
std::swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void push(const T& val)
{
_con.push_back(val);
adjust_up(_con.size() - 1);
}
void pop()
{
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
}
const T& top()const
{
return _con[0];
}
size_t size()const
{
return _con.size();
}
bool empty()const
{
return _con.empty();
}
private:
Container _con;
};
}
on.push_back(val);
adjust_up(_con.size() - 1);
}
void pop()
{
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
}
const T& top()const
{
return _con[0];
}
size_t size()const
{
return _con.size();
}
bool empty()const
{
return _con.empty();
}
private:
Container _con;
};
}