【C++】STL——priority_queue的使用与底层模拟实现
前言
本文详细讲解STL库中priority_queue的使用及底层模拟实现,更多STL有关内容可查看C++专栏部分,文章文档主要参考https://legacy.cplusplus.com/。
1.优先级队列
1.1认识优先级队列
优先级队列是一种容器适配器,主要是将容器中的数据按照优先级的先后来进行排列,优先级越高会越靠近top位置,因此也可抽象为一种类堆结构。
比如将数据按照数值越大的优先级越高,那么最后就会呈现一种类似大堆的结构。
1.2优先级队列的使用
STL中优先级队列提供的接口较少,C++98开始提供的接口只有插入(push)、删除(pop)、判空(empty)、获取顶部元素(top)、获取元素个数(size)。
对优先级队列的简单使用可以通过实例化对象后再逐个插入数据,随后通过判空条件再循环取顶部元素、删除顶部元素来进行数据的获取。代码如下
#include<iostream>
#include<queue>
using namespace std;void test01()
{//类堆结构priority_queue<int> pq;pq.push(3);pq.push(1);pq.push(2);pq.push(99);while (!pq.empty()){cout << pq.top() << ' ';pq.pop();}cout << endl;
}int main()
{test01();return 0;
}
运行结果如下:
可以看到这里的顶部元素的从大到小,因此默认是大堆。那如果想要是个小堆呢?再看看文档中优先级队列的模板参数
可以看到除了容器数据类型,所选底层容器,还有一个Compare的模板参数默认是less。这里的Compare模板参数可以类似于C语言中的函数指针进行函数回调功能,C++中的这种函数回调方式称为仿函数,其语法和使用并没函数指针那么复制。
通过默认的less仿函数可以进行大堆的创建,想要建小堆就要用其相反的greater仿函数。但传递模板参数时要注意将容器参数也要传进去。
void test02()
{priority_queue<int, vector<int>, greater<int>> pq;pq.push(3);pq.push(1);pq.push(2);pq.push(99);while (!pq.empty()){cout << pq.top() << ' ';pq.pop();}cout << endl;
}
运行结果如下:
可以看到传了greater参数后,建的就是小堆了。
2.仿函数
2.1认识仿函数
仿函数是指任何定义了 operator() 的对象。换句话说,它是一个行为像函数的类实例化对象
类似C++标准库中的less,我们可以写一个自己的Less仿函数:
template<class T>
class Less
{
public:bool operator()(const T& x, const T& y){return x < y;}
};void test03()
{int a = 1, b = 100;Less<int> com;cout << com(a, b) << endl; //实例化对象仿函数调用cout << Less<int>()(a, b) << endl; //匿名对象仿函数调用
}
运行结果如下:
可以看到仿函数的调用遵循类对象成员函数的调用规则。
2.2类模板与函数模板的仿函数传参
通过仿函数,我们也能进行函数回调的实现,这里拿STL库中的sort函数举例:
sort函数重载了一份第二个参数可以控制比较方式函数,这样我们通过传比较仿函数,就能实现我们想要的排序效果:
#include<vector>
#include<algorithm>void test04()
{vector<int> nums = { 3,1,12,21,8,6 };sort(nums.begin(), nums.end(), greater<int>());
}
但这里最值得注意的是sort函数后面的是greater<int>(),与priority_queue传的greater<int>是不同的。主要原因是函数传模板参数时传的实际对象,而类模板传参数时传的是类型(也就是类),使用时要注意实际区分。
3.priority_queue的模拟实现
3.1框架构思
要仿照STL结构复刻出一个类优先级队列结构,首先要清楚优先级队列是一个容器适配器,因此底层成员就只需要一个容器;随后就是对STL中基本接口(push、pop、empty等)模拟复现;最后就要处理一些特殊场景,进而对功能进行完善和优化。
3.2代码实现
priority_queue的创建
类的创建过程中最需要注意的就是类的内部成员变量以及默认成员函数,以及为了确保代码的泛用性需要采用合适的模板,这里需要的就有变量类型、容器选择、仿函数(比较函数)选择的模板需求。成员变量为容器即可,而对于默认成员函数无需特别实现,像构造函数会直接调用容器的构造。
代码实现如下:
namespace mystl
{template <class T, class Container = vector<T>, class Compare = Less<T>>class priority_queue{public:private:Container _con; //容器适配器};
}
创建好后再对基本接口进行逐步完善:
push的实现
在优先级队列实现插入功能跟数据结构中的堆(heap)类似,先尾插一个数据,再进行向上调整。尾插功能在容器中可以直接调用,因此主要对向上调整算法进行实现。
在实现向上调整算法前首先回忆完全二叉树中父节点和孩子节点的下标关系:
• 对于具有 n 个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的数组顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:1. 若 i>0 , i 位置结点的双亲序号:(i-1)/2; i=0 , i 为根结点编号,⽆双亲结点2. 若 2i+1<n ,左孩⼦序号: 2i+1 , 2i+1>=n 则⽆左孩⼦3. 若 2i+2<n ,右孩⼦序号: 2i+2 , 2i+2>=n 则⽆右孩⼦
private://向上调整void adjust_up(size_t child){Compare com; //仿函数size_t parent = (child - 1) / 2;while (child > 0){// 大堆:less// 小堆:greaterif (com(_con[parent], _con[child])){swap(_con[parent], _con[child]);}else{break;}child = parent;parent = (child - 1) / 2;}}void push(const T& x)
{//尾插_con.push_back(x);//向上调整adjust_up(_con.size() - 1);
}
细节:这里循环条件不能用parent >= 0,因为parent为size_t类似,是绝对不会为负数的。其次为了代码的复用性这里的比较用的是仿函数,方便后面进行需求建堆。
pop的实现
删除的基本思路是将顶部元素与尾元素进行交换,然后进行容器的尾删操作,因根节点的调整导致堆结构破坏,因此要从根节点开始进行向下调整,恢复堆结构。
向下调整
向下调整是先让选出父节点中优先级最高的孩子节点,然后让父节点与该孩子节点进行比较,(以大堆为例)只要父节点比孩子节点小,那么就让父节点与孩子节点进行交换,直到重新变为大堆。
//向下调整
private:void adjust_down(size_t parent){Compare com;//仿函数size_t child = parent * 2 + 1;while (child < _con.size()){// 大堆: less// 小堆: greaterif (child + 1 < _con.size() && com(_con[child], _con[child + 1])){++child;}// 大堆:less// 小堆:greaterif (com(_con[parent], _con[child])){swap(_con[child], _con[parent]);}else{break;}parent = child;child = parent * 2 + 1;}public:void pop()
{assert(!empty());//首尾交换swap(_con[0], _con[_con.size() - 1]);_con.pop_back(); //尾删//向下调整adjust_down(0);
}
size接口的实现
size接口主要是返回容器中的元素个数,方法有很多,但最直接的就是调用容器的size接口。
size_t size()const
{return _con.size();
}
empty接口的实现
empty接口主要是判断容器内元素是否为空,也可以直接调用容器的empty。
bool empty()const
{return _con.empty();
}
top接口的实现
top接口返回优先级高的元素,即返回下标为0的元素,但要注意若容器为空,那么是无法进行获取的。
T top()const
{assert(!empty());return _con[0];
}
完成实现后就是对代码进行测试,最简单的测试就是用刚开始标准库中的使用部分,但注意要在模拟实现文件中补充Less和Greater仿函数。
//仿函数
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;}
};
测试如下:
//mystl测试
#include<iostream>
#include"priority_queue.h"
using namespace std;namespace mystl
{void test01(){//类堆结构priority_queue<int> pq;pq.push(3);pq.push(1);pq.push(2);pq.push(99);while (!pq.empty()){cout << pq.top() << ' ';pq.pop();}cout << endl;}
}int main()
{mystl::test01();
}
运行结果如下:
构造函数完善(迭代器构造)
在容器使用时我们不可能通过一个个数据的push来初始化堆数据,这时我们可以通过STL封装的迭代器来进行构造初始化。要实现迭代器构造初始化,可以先用迭代器区间数据来初始化容器,然后用建堆算法来将非堆数组变为堆。
建堆
将非堆数组变为堆需要从最小子树开始将每棵子树依次进行向下调整建堆。
priority_queue() = default;//迭代器构造
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last):_con(first, last)
{//建堆for (int i = ((int)_con.size() - 2) / 2; i >= 0; i--){adjust_down(i);}
}
这里需要注意的就是当我们写了构造函数之后,编译就不会再生成默认构造,这时我们需要强制让编译器生成默认构造(priority_queue() = default;)。
3.3最终代码展示
#pragma once
#include<vector>
#include<assert.h>
#include<algorithm>
using namespace std;namespace mystl
{//仿函数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 Compare = Less<T>>class priority_queue //类堆结构{public:priority_queue() = default;//迭代器构造template<class InputIterator>priority_queue(InputIterator first, InputIterator last):_con(first, last){//建堆for (int i = ((int)_con.size() - 2) / 2; i >= 0; i--){adjust_down(i);}}void push(const T& x){//底层数组尾插_con.push_back(x);//向上调整adjust_up(_con.size() - 1);}void pop(){assert(!empty());//首尾交换swap(_con[0], _con[_con.size() - 1]);_con.pop_back();//向下调整adjust_down(0);}T top()const{assert(!empty());return _con[0];}bool empty()const{return _con.empty();}size_t size()const{return _con.size();}private:Container _con;private://向上调整void adjust_up(size_t child){Compare com;size_t parent = (child - 1) / 2;while (child > 0){// 大堆:<// 小堆:>if (com(_con[parent], _con[child])){swap(_con[parent], _con[child]);}else{break;}child = parent;parent = (child - 1) / 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])){swap(_con[child], _con[parent]);}else{break;}parent = child;child = parent * 2 + 1;}}};
}