目录
一,优先队列简介
二,priority_queue 的内部实现原理
三,模拟实现 priority_queue
1,模板参数与数据结构
2,构造
3,辅助功能(堆的有序化,建立堆)
4,核心功能
四,简单的测试 priority_queue 与完整代码
一,优先队列简介
1,什么是优先队列:
优先队列是一种特殊的队列数据结构,它具有队列先进先出(FIFO)的特性,但是在取出元素时会优先取出具有最高优先级的元素。这意味着插入的元素会按照一定的优先级规则进行排序,而不是简单地按照插入顺序。
2,STL中的优先队列:
在 C++ 的 STL 中,priority_queue 是一个容器适配器,用于实现优先队列的功能。priority_queue 提供了插入元素、移除最高优先级元素、访问堆顶元素等常用操作,操作十分便捷。
3,优先队列的使用场景:
① 任务调度:在操作系统中,任务调度往往需要根据各个任务的优先级来决定执行顺序,优先队列可以很好地满足这种场景。
② 事件处理:在网络编程或并发编程中,事件处理的顺序可能会影响程序性能,优先队列可以帮助我们按照一定的规则处理事件顺序。
③ 一些图论算法:很多图论算法(例如 Dijkstra 算法)中都需要根据权值来确定优先级,这时候可以使用优先队列来保存待处理的顶点,并按照权值进行排序。
优先队列示意图:
二,priority_queue 的内部实现原理
1,堆结构
优先队列通常是基于二叉堆来实现的,二叉堆能够很好的实现优先队列的基本操作(为方便表述,接下来将二叉堆简称为堆)。那么,什么是堆呢?
堆(Heap)是一种特殊的树形数据结构,根据不同的排列方式通常可以分为最大堆和最小堆两种类型。以最大堆为例:它的特点是父节点的键值总是大于或等于任何一个子节点的键值。这种性质决定了堆的根节点(也就是堆顶)的键值是最大的。
堆通常是一个完全二叉树,除了最底层,其它层全部都会被填充满,最底层从左到右填充。我们用数组就可以很方便地表示一个堆,而且堆的操作也多是通过数组的索引进行的。
二叉堆示意图:
2,二叉堆的表示
如果我们用指针来表示堆结构,那么每个元素都需要左子节点,右子节点,父节点三个指针;而如果我们使用数组来表示,就会变得特别方便,不需要指针就可以沿着树上下的移动。具体方式如下:arr[0] 可以用来当作根节点,对于 arr[k],它的左右两个子节点的索引分别为 2k + 1 ,2k + 2,它的父节点的索引则为 (k - 1) / 2。
(有些表示方法用 arr[1] 来作为根节点,而 arr[0] 则用来作为哨兵闲置)
用数组表示堆的示意图:
三,模拟实现 priority_queue
priority_queue 在 STL 中以底部的容器来完成几乎全部的工作,所以它并没有被归类为容器(container),而是被归类为容器适配器(container adapter)。
priority_queue 只有队列中的顶部元素(优先级最高的元素)才能被外界取用,所以 priority_queue 不提供遍历功能,这也代表着它并不提供迭代器。
1,模板参数与数据结构
template<class T, class Compare = std::less<T>, class Container = std::vector<T>>
class priority_queue {
private:
Container _cont; // 底层容器
Compare _cmp; // 比较方式
// ...
}
前面有说到我们会用数组来作为底层容器来表示堆,为了方便底层容器的扩容,这里选择缺省使用 vector 来作为底层容器。
2,构造
public:
// 构造
priority_queue() {}
template<class input_iterator>
priority_queue(input_iterator begin, input_iterator end) : _cont(begin, end) {
make_heap();
}
priority_queue(const std::initializer_list<T>& lt) :_cont(lt) {
make_heap();
}
make_heap() 函数会将 _cont 中的元素给调整成一个堆,具体实现稍后就会给出。
3,辅助功能(堆的有序化,建立堆)
首先是寻找父节点与子节点的索引功能:
private:
size_t parent_idx(size_t idx) { return (idx - 1) / 2; }
size_t left_child_idx(size_t idx) { return 2 * idx + 1; }
size_t right_child_idx(size_t idx) { return 2 * idx + 2; }
我们对优先队列进行一些操作时会破环堆的结构,之后我们会遍历堆,将堆的结构给恢复回来。这个恢复的过程就叫做堆的有序化。
堆的有序化分为两种情况:
① 当某个节点的优先级上升,或者是我们在堆底(底层容器的尾部)加入了一个新的元素时,我们需要自下而上的对堆进行调整。
② 当某个节点的优先级下降时,我们需要自上而下的对堆进行调整。
先来看看自下而上的调整:
private:
void adjust_up_heap(size_t child) {
size_t parent = parent_idx(child);
while (child != 0 and _cmp(_cont[parent], _cont[child])) {
std::swap(_cont[parent], _cont[child]);
child = parent;
parent = parent_idx(child);
}
}
如果堆的结构因为某个节点比它的父节点优先级更高而被打破,那么我们就需要交换当前的节点与父节点。不过交换之后可能还会在父节点处继续打破堆的结构,所以我们要将父节点更新为当前节点,继续进行交换,直到堆恢复完成。
再来看看自上而下的调整:
private:
void adjust_down_heap(size_t parent) {
size_t n = _cont.size();
size_t child = left_child_idx(parent);
while (child < n) {
if (child + 1 < n and _cmp(_cont[child], _cont[child + 1])) {
++child; // 将 left_child 变为 right_child
}
if (_cmp(_cont[parent], _cont[child])) {
std::swap(_cont[parent], _cont[child]);
parent = child;
child = left_child_idx(parent);
}
else break;
}
}
如果堆的结构因为某个节点比它的子节点优先级更小而被打破,那么我们就需要将它和它的两个子节点中优先级更高的那一位进行交换。同样的,交换之后子节点处可能还会破环堆的结构,所以我们需要将子节点更新为当前节点,继续进行交换,直到堆恢复完成。
引用《算法 (第四版)》 中的一段话:
如果我们把堆想象成一个严密的黑社会组织,每个子结点都表示一个下属(父结点则表示它的直接上级),那么这些操作就可以得到很有趣的解释。adjust_up_heap() 表示一个很有能力的新人加入组织并被逐级提升(将能力不够的上级踩在脚下),直到他遇到了一个更强的领导。adjust_donw_heap() 则类似于整个社团的领导退休并被外来者取代之后,如果他的下属比他更厉害,他们的角色就会交换,这种交换会持续下去直到他的能力比其下属都强为止。
最后我们来看看怎么将一个无序的容器调整成一个堆:
private:
void make_heap() {
size_t tail_child = _cont.size() - 1;
size_t parent = parent_idx(tail_child);
size_t stop = -1;
while (parent != stop) {
adjust_down_heap(parent--);
}
}
我们从最尾部的元素的父节点开始向上遍历,每次都将当前的节点进行向下调整操作。
4,核心功能
priority_queue 的核心功能如下:插入元素(push),删除顶部元素(pop),获取顶部元素(top),检查优先队列是否为空(empty),获取元素数量(size)。
public:
// 核心功能
void push(const T& data) {
_cont.push_back(data);
adjust_up_heap(_cont.size() - 1);
}
void pop() {
_cont.front() = _cont.back();
_cont.pop_back();
adjust_down_heap(0);
}
T& top() { return _cont.front(); }
size_t size() { return _cont.size(); }
bool empty() { return _cont.empty(); }
void swap(const priority_queue& pq) {
std::swap(_cont, pq._cont);
std::swap(_cmp, pq._cmp);
}
四,简单的测试 priority_queue 与完整代码
1,简单的对 priority_queue 进行测试
我们可以大量的对 priority_queue 进行插入,然后再一个一个的取出顶部的元素。
void test_priority_queue() {
std::vector<int> vec;
for (int i = 0;i < 100;++i) {
vec.push_back(rand() % 60);
}
mySTL::priority_queue<int>heap(vec.begin(), vec.end());
heap.push(100);
while (not heap.empty()) {
cout << heap.top() << " ";
heap.pop();
}
}
运行的结果:
2,完整代码
namespace mySTL {
template<class T, class Compare = std::less<T>, class Container = std::vector<T>>
class priority_queue {
private:
Container _cont; // 底层容器
Compare _cmp; // 比较方式
public:
// 构造
priority_queue() {}
template<class input_iterator>
priority_queue(input_iterator begin, input_iterator end) : _cont(begin, end) {
make_heap();
}
priority_queue(const std::initializer_list<T>& lt) :_cont(lt) {
make_heap();
}
public:
// 核心功能
void push(const T& data) {
_cont.push_back(data);
adjust_up_heap(_cont.size() - 1);
}
void pop() {
_cont.front() = _cont.back();
_cont.pop_back();
adjust_down_heap(0);
}
T& top() { return _cont.front(); }
size_t size() { return _cont.size(); }
bool empty() { return _cont.empty(); }
void swap(const priority_queue& pq) {
std::swap(_cont, pq._cont);
std::swap(_cmp, pq._cmp);
}
private:
size_t parent_idx(size_t idx) { return (idx - 1) / 2; }
size_t left_child_idx(size_t idx) { return 2 * idx + 1; }
size_t right_child_idx(size_t idx) { return 2 * idx + 2; }
private:
void make_heap() {
size_t tail_child = _cont.size() - 1;
size_t parent = parent_idx(tail_child);
size_t stop = -1;
while (parent != stop) {
adjust_down_heap(parent--);
}
}
void adjust_down_heap(size_t parent) {
size_t n = _cont.size();
size_t child = left_child_idx(parent);
while (child < n) {
if (child + 1 < n and _cmp(_cont[child], _cont[child + 1])) {
++child; // 将 left_child 变为 right_child
}
if (_cmp(_cont[parent], _cont[child])) {
std::swap(_cont[parent], _cont[child]);
parent = child;
child = left_child_idx(parent);
}
else break;
}
}
void adjust_up_heap(size_t child) {
size_t parent = parent_idx(child);
while (child != 0 and _cmp(_cont[parent], _cont[child])) {
std::swap(_cont[parent], _cont[child]);
child = parent;
parent = parent_idx(child);
}
}
//public:
// // 方便调试
// void print() {
// for (const auto& data : _cont) {
// std::cout << data << " ";
// }
// std::cout << std::endl;
// }
};
}