📄前言
不知你是否有听说过priority_queue,它是C++的内置类,又名优先级队列。学习其能帮助你解决许多按照优先级排序的问题。如果你还没有学习过优先级队列,那么本文将带领你走向其第一步。
priority_queue
概念
priority_queue 是一种优先级队列的容器,优先级队列是一种自动排序的容器,每次插入元素都会为其安排到一个适当的位置,以让队列保持有序。
- priority_queue是一种模板类,默认使用vector作为适配器
- priority_queue的头节点永远都是最大/小的。
priority_queue的主要成员函数:
函数 | 说明 |
---|---|
priority_queue() | 构建一个空的优先队列 |
priority_queue(first, last) | 利用迭代器来构造优先队列 |
empty() | 判断是否为空 |
top() | 在优先级队列插入元素x |
pop() | 删除队列中最大/小的元素 |
容器适配器
什么是适配器
priority_queue可以使用deque或vector作为其底层实现的适配器。
适配器是一种设计模式,用于将一个接口转换成另一个接口,以便不兼容的类可以协同工作。著名的stl标准库中也使用了适配器模式,如queue、stack、priority_queue等模板类就是如此。
代码部分
priority_queue一般使用数据结构 堆 来进行实现,所以一般需要用到堆的时候,都可以考虑使用priority_queue。
// priority是一种模板类,可以支持伪函数
// 这三个参数分别是数据类型,适配器,伪函数(用于数据比较)
template <class T, class Container = std::vector<T>, class compare = std::less<T>>
class priority_queue
{
public:
// -----成员函数 --------
private:
Container con; //适配器
Compare comp; //伪函数
};
插入结点
我们尝试可以把堆的空间想像成一颗完全二叉树,尽管它的底层是一个数组,我们仍可以通过公式来转换。
- 当前位置为 i i i,则左结点为 ( i ∗ 2 ) + 1 (i*2) + 1 (i∗2)+1 ,右结点为 ( i ∗ 2 ) + 2 (i * 2) + 2 (i∗2)+2;
- 当前位置为 i i i,则父结点位置为 ( i − 1 ) / 2 (i - 1) / 2 (i−1)/2。
堆的每次插入都要与其父结点比较,如果子节点比父结点大/小就和父结点交换位置。
void push(const T& x)
{
c.push_back(x); //适配器的接口复用
adjust_up(size() - 1);
}
//向上调整
void adjust_up(size_t child)
{
size_t parent = (child - 1) / 2;
while(child > 0)
{
//利用伪函数进行比较
if(comp(con[parent], con[child))
{
std::swap(con[child], con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
//迭代器版本的构造函数
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
:con(first, last)
{
for(int i = (size()-2) / 2; i >= 0; --i)
{
adjust_down(i);
}
}
删除结点
删除结点就等于把头结点给抛出,为了保持堆的正确性,需要重新建堆,也就是需要把最后的结点给回头结点,再依次比较子节点和父结点的大小,直到结点回到正确位置。
void pop()
{
std::swap(con[0], con[size()-1]);
con.pop_back();
adjust_down(0);
}
//向下调整
void adjust_down(int parent)
{
int child = parent * 2 + 1;
while(child < size())
{
if(child + 1 < size() && comp(con[child], con[child+1]))
child++; //选出左右孩子最大/小的那个
if(comp(con[parent], con[child]))
{
std::swap(con[child], con[parent]);
parent = child;
child = child * 2 + 1;
}
else
break;
}
}
其余函数
剩余的成员函数都是可以用适配器的接口来复用的,所以省去了很多工作。
bool empty() const
{
return con.empty();
}
size_t size() const
{
return con.size();
}
//需要为const对象准备一份
const T& top() const
{
return con[0];
}
T& top()
{
return con[0];
}
📓总结
需要完整代码,可以到我的github去取
接口 | 时间复杂度 |
---|---|
push() | O ( l o g 2 N ) O(log_2N) O(log2N) |
pop() | O ( l o g 2 N ) O(log_2N) O(log2N) |
top() | O ( 1 ) O(1) O(1) |
empty() | O ( 1 ) O(1) O(1) |