1. 行为:堆逻辑
优先队列类似堆这种数据结构,在堆中可以随时插入和删除元素,并且可以访问堆顶元素。
堆的C语言实现方式和基础知识可以阅读此篇博客,这里不再过多赘述:
2. 实现方式:容器适配器
优先队列是一种容器适配器,其底层容器默认使用vector,并对它进行"适配":
- 保留vector的连续存储能力
- 增加了堆的优先级规则
- 屏蔽了随机访问的操作,仅仅提供了访问堆顶元素的接口
注意,底层容器应该具有以下操作权限:
- **empty():**检测容器是否为空
- **size():**获取容器中元素大小
- **front():**返回容器首元素
- **push_back():**在容器尾部插入元素
- **pop_back():**删除尾部元素
另外,容器还需支持随机访问迭代器,因为堆的核心操作离不开快速访问父子节点和两两交换节点元素。
vector和deque都可以满足上述条件,但由于deque的存储不完全连续,频繁访问的效率较低,因此设置vector为默认的底层容器。
这里设置优先队列的私有成员变量为底层容器和比较逻辑:
cpp
private:
Container c; //底层容器
Compare comp; //比较规则
3. 模拟实现
3.1 核心操作和相关知识
3.1.1 构造优先队列
将一个容器中的元素整理为堆,标准做法是传入容器的首尾迭代器,然后建堆。
建堆的两种常见方式:
- 复用库中的堆算法
- 从最后一个非叶子节点向下调整建堆
在模拟实现中我们选择第二种,可以更清晰的展示堆的构建逻辑:
cpp
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
:c(first, last)
{
//建堆
// make_heap(c.begin(), c.end(), comp);
for (int i = (c.size()-1-1)/2; i >= 0; i--)
{
adjust_down(i);
}
}
3.1.2 push
**功能:**尾部插入元素,并整理为堆。
具体实现步骤:
- 底层容器对元素进行尾插操作
- 堆的向上调整算法
代码如下:
cpp
void push(const T& x)
{
c.push_back(x);
//库中堆算法: push_heap(c.begin(), c.end(), comp);
adjust_up(c.size() - 1);
}
//手动实现的向上调整算法
void adjust_up(size_t child)
{
while (child > 0)
{
size_t parent = (child - 1) / 2;
if (comp(c[parent], c[child]))
{
break;
}
swap(c[child], c[parent]);
child = parent;
}
}
3.1.3 pop
**功能:**删除堆顶元素
具体实现步骤:
- 交换首尾元素
- 删除尾部元素
- 对新的堆顶元素进行向下调整算法
代码如下:
cpp
void pop()
{
//堆算法实现交换和调整: pop_heap(c.begin(), c.end(), comp);
swap(c[0], c[c.size() - 1]);
adjust_down(0);
c.pop_back();
}
// 手动实现的向下调整算法
void adjust_down(size_t parent)
{
size_t child = parent * 2 + 1;
while (child < c.size())
{
if (child + 1 < c.size() && comp(c[child], c[child + 1]))
{
++child;
}
if (comp(c[parent], c[child]))
{
break;
}
swap(c[parent], c[child]);
parent = child;
child = parent * 2 + 1;
}
}
3.1.4 比较逻辑
由于模板中的默认比较逻辑是小于,因此优先队列默认创造大堆。在模拟实现时需要额外留意向上和向下调整算法的比较结果。
优先队列的模板如下:
cpp
//默认vector,大堆
template< class T, class Container = std::vector<T>, class Compare = less<T> >
如果需要创建小堆,则需另外传入比较逻辑greater<T>:
(以int为例)
cpp
vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
//默认大堆
priority_queue<int> q1(v.begin(), v.end());
//小堆
priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
如果需要显式传入比较逻辑,则第二个参数不能省略,不能跳过一个有默认值的参数,去指定后面的参数!
另外,如果堆中的元素为自定义类型 ,则需要重载<和>运算符:大堆重载<,小堆重载>
3.1.5 仿函数
仿函数的本质是类 ,重载了operator(),从而使对象可以像函数一样使用,因此被称为仿函数。
优先队列里体现在类型less<T>和greater<T>,这是C++库里提供的"仿函数类",使用时需要引入<functional>头文件。简化的实现逻辑如下:
cpp
template <typename T>
struct MyLess {
bool operator()(const T& a, const T& b) const
{
return a < b;
}
};
优先队列内部会创建这个类型的对象:Compare comp;
随后在成员函数中调用这个对象,就可以发挥比较的功能了。例如之前提到的向上调整算法:
cpp
void adjust_up(size_t child)
{
while (child > 0)
{
size_t parent = (child - 1) / 2;
if (comp(c[parent], c[child])) //小于
{
break;
}
swap(c[child], c[parent]);
child = parent;
}
}
仿函数可以通过创建临时对象 使用,也可以通过实例化对象使用:
cpp
//创建临时对象
greater<int>()(a, b);
//实例化对象
greater<int> comp;
comp(a, b);
优先队列的模拟实现中采用了实例化的方式。
3.2 代码实现
cpp
#include<iostream>
#include<vector>
#include<functional>
using namespace std;
namespace tairitsu_h
{
template< class T, class Container = std::vector<T>, class Compare = less<T> >
class priority_queue
{
public:
priority_queue()
{}
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
:c(first, last)
{
//建堆
// make_heap(c.begin(), c.end(), comp);
for (int i = (c.size()-1-1)/2; i >= 0; i--)
{
adjust_down(i);
}
}
bool empty() const
{
return c.empty();
}
size_t size() const
{
return c.size();
}
T& top() const
{
return c[0];
}
void push(const T& x)
{
c.push_back(x);
//库中堆算法: push_heap(c.begin(), c.end(), comp);
adjust_up(c.size() - 1);
}
void pop()
{
//堆算法实现交换和调整: pop_heap(c.begin(), c.end(), comp);
swap(c[0], c[c.size() - 1]);
adjust_down(0);
c.pop_back();
}
private:
Container c; //底层容器
Compare comp; //比较规则
void adjust_up(size_t child)
{
while (child > 0)
{
size_t parent = (child - 1) / 2;
if (comp(c[parent], c[child]))
{
break;
}
swap(c[child], c[parent]);
child = parent;
}
}
void adjust_down(size_t parent)
{
size_t child = parent * 2 + 1;
while (child < c.size())
{
if (child + 1 < c.size() && comp(c[child], c[child + 1]))
{
++child;
}
if (comp(c[parent], c[child]))
{
break;
}
swap(c[parent], c[child]);
parent = child;
child = parent * 2 + 1;
}
}
};
};
//鸽了比较久,在忙学校的一些事ww
//封面:凯尔希(❁´◡`❁)