priority_queue类的使用及介绍、模拟实现
priority_queue类的介绍
1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
2. 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
- empty():检测容器是否为空
- size():返回容器中有效元素个数
- front():返回容器中第一个元素的引用
- push_back():在容器尾部插入元素
- pop_back():删除容器尾部元素
5. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。因为相比deque,vector的随机访问效率更高。
6. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
priority_queue类的使用
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。
1.默认情况下,priority_queue是大堆。
2.建堆的两种方式
- 方式1:通过不断尾插数据建堆
- 方式2:基于已有容器的迭代器区间建堆
3.priority_queue(堆)的遍历
4. 如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载。
仿函数
1.仿函数的定义
仿函数(Function Object)是一种重载了 operator()
的类或结构体,它允许对象像函数一样被调用。
#include<iostream>
using namespace std;
//仿函数类模板
//注:仿函数本质是可以让对象像函数一样使用
template<class T>
struct Less
{
//对象通过重载operator()就可以让对象 像 函数一样使用,但是仿函数本质还是个对象。
bool operator()(const T& x, const T& y)//通过重载()括号运算符来实现仿函数即函数对象。
{
return x < y;
}
};
int main()
{
//测试仿函数类模板
Less<int> lessFunc;//函数对象
//通过重载operator()就可以让对象 像 函数一样使用,但是本质是这个对象在调用operator()。
cout << lessFunc(1, 2) << endl;
//注:单看lessFunc(1, 2)以为是个函数指针 或者 是个函数名,但是实际是个函数对象。
return 0;
}
2.仿函数与 priority_queue 的关系
在 C++ 的 priority_queue
中,仿函数用于定义元素的优先级比较规则,直接决定堆的类型(大堆或小堆)。
(1)priority_queue 的模板参数解析
template <
class T,
class Container = vector<T>,
class Compare = less<typename Container::value_type>
> class priority_queue;
Compare
参数:- 默认使用
less<T>
(大堆),元素按降序排列。 - 若需小堆,需显式指定
greater<T>
。 - 自定义仿函数可实现复杂比较逻辑(如结构体按成员排序)。
- 默认使用
(2) 仿函数控制堆的行为
仿函数类型 | 比较规则 | 堆类型 | 堆顶元素 |
---|---|---|---|
less<T> | a < b 时返回 false | 大堆 | 最大值 |
greater<T> | a > b 时返回 false | 小堆 | 最小值 |
自定义仿函数 | 用户定义的任意比较逻辑 | 自定义堆 | 根据逻辑决定 |
示例:大堆与小堆的构建
//大堆(默认)
priority_queue<int> max_heap;
//小堆(显式指定greater)
priority_queue<int, vector<int>, greater<int>> min_heap;
(3)自定义仿函数的应用场景
当处理自定义数据类型(如结构体)时,需通过自定义仿函数定义比较规则:
struct Student
{
string name;
int score;
};
//按分数降序排序(大堆)
struct CompareByScore
{
bool operator()(const Student& a, const Student& b) const
{
return a.score < b.score; // 注意:这里的逻辑与less<T>一致
}
};
按学生名字降序排序(大堆)
//struct CompareByName
//{
// bool operator()(const Student& a, const Student& b) const
// {
// //字符串比较是字典序,返回a.name > b.name表示降序
// return a.name > b.name;
// }
//};
priority_queue<Student, vector<Student>, CompareByScore> pq;
priority_queue类模板的模拟实现
priority_queue 模板类概述
priority_queue
(优先队列)是一种容器适配器,底层通常基于堆(完全二叉树)实现。在 C++ 中,其模板定义如下:
template<class T, class Container = vector<T>,
class Compare = less<typename Container::value_type>>
class priority_queue;
其中,T
是存储元素的类型;Container
是底层容器类型,默认是vector<T>
,也可以是deque<T>
等;Compare
是用于比较元素优先级的仿函数,默认是less<T>
,意味着默认构建大堆(即每个父节点的值都大于或等于其子节点的值)。若将Compare
指定为greater<T>
,则构建小堆(即每个父节点的值都小于或等于其子节点的值)。
注意事项:
- 仿函数本质是通过在类中重载
operator()
运算符,使得类的对象可以像普通函数一样被调用 ,因此仿函数也被称为函数对象。 - 对于优先级队列(堆)
priority_queue
,它是一种容器适配器。在其底层实现中,既可以通过封装vector<T>
,也可以通过封装deque<T>
来完成。但前提是,作为底层容器的vector<T>
或者deque<T>
必须具备priority_queue
所需的基本函数接口,例如empty
(用于判断容器是否为空)、size
(用于获取容器中元素的数量)、top
(用于访问堆顶元素,即优先级最高的元素 )、push
(用于向容器中插入元素)、pop
(用于移除堆顶元素)等。只有当vector<T>
或者deque<T>
满足这些功能要求时,它们才能够作为priority_queue
的容器适配器。
双亲和孩子下标关系
堆通常用数组实现,通过数组下标可以便捷地找到双亲和孩子节点的对应关系:
1.已知双亲下标求孩子下标:
- 若双亲下标为
parent
,则左孩子下标leftchild = 2 * parent + 1
。 - 右孩子下标
rightchild = 2 * parent + 2
。 - 注意:如果计算出的孩子下标超出数组的有效范围,则说明当前双亲不存在对应的孩子。在完全二叉树中,双亲的左右孩子在数组中是连续存储的,且左孩子下标一定小于右孩子下标。
2.已知孩子下标求双亲下标:
- 已知左孩子下标
leftchild
,双亲下标parent = (leftchild - 1) / 2
。 - 已知右孩子下标
rightchild
,双亲下标parent = (rightchild - 2) / 2
。 - 若只知道孩子下标
child
,无论它是左孩子还是右孩子,双亲下标都可以通过parent = (child - 1) / 2
计算得到 。这是因为在整数除法中,小数部分会被舍去,所以两种情况下计算结果相同。
构造函数
在模拟实现 priority_queue
类模板时,通常不需要自定义实现赋值重载、拷贝构造、析构函数,原因如下:
priority_queue
属于容器适配器,其底层依赖诸如vector
、deque
等其他容器来存储数据。而这些底层容器自身已经完整地实现了拷贝构造函数、赋值运算符重载以及析构函数。- 当我们进行
priority_queue
类模板的实现时,若没有显式地自定义实现赋值运算符重载、拷贝构造函数和析构函数,编译器会自动为该类生成默认的拷贝构造函数、赋值运算符重载和析构函数。这些默认生成的函数会自动调用底层容器对应的成员函数,进而对priority_queue
中作为自定义类型成员变量的底层容器完成拷贝、赋值和析构等操作。
默认构造函数
//创造空的优先级队列
//构造函数:初始化底层容器,创建一个空的优先队列对象
priority_queue()
: _con()
{}
实现思路:直接调用底层容器(如 vector)的默认构造函数,初始化 _con,使创建的 priority_queue 对象内部没有存储任何元素,成为一个空的优先队列。
注意事项:
- 提供无参构造方式,满足用户创建空优先队列的基础需求,是优先队列最基础的构造接口,后续可通过
push
等操作填充元素。 - 这里显示写默认构造而不是用编译器会自动生成默认构造函数的原因:当类中没有任何构造函数时,编译器会自动生成默认构造函数。但若用户定义了任意一个构造函数(如迭代器区间构造函数),编译器不再生成默认构造函数。此时,类缺少默认构造函数。若此时用户没有显式写默认构造函数,当用户尝试创建空的优先队列(如
priority_queue<int> pq;
)时,编译器找不到匹配的构造函数,会报 “没有合适的构造函数” 的编译错误。 - 总的来说,由于存在迭代器区间构造函数(用户自定义构造函数),必须显式编写默认构造函数
priority_queue() : _con() {}
。否则,用户无法通过priority_queue<T> pq;
的方式创建空的优先队列对象,导致编译失败。
迭代器区间构造函数(建堆)
1.思路1:利用向下调整建堆(推荐)
//迭代器区间构造函数:根据给定的迭代器范围 [first, last) 构造优先队列,并完成建堆
//实现思路:先将 [first, last) 区间元素插入底层容器,再通过向下调整算法构建堆
template<class Iterator>
priority_queue(Iterator first, Iterator last)
: _con(first, last)
{
int size = _con.size();
//利用向下调整来建堆
//最后一个结点的双亲:通过最后一个孩子下标 (size - 1) 计算出双亲下标 (size - 2) / 2
for (int parent = (size - 1 - 1) / 2; parent >= 0; parent--)
adjust_down(parent); //对非叶子结点依次向下调整,构建堆
}
实现思路:
- 先利用底层容器的迭代器区间构造函数
_con(first, last)
,将[first, last)
区间元素存入 底层容器_con 中
。此时数据仅完成存储,尚未满足堆结构。 - 对底层容器中的元素建堆:从最后一个非叶子结点((size - 2) / 2)开始,依次对每个结点执行向下调整算法,最终构建满足大堆性质的优先队列。
优势:
- 时间复杂度优:向下调整建堆的时间复杂度为 O(n),比逐个元素插入(向上调整)的 O(nlogn) 效率更高,适合批量元素初始化场景。
- 保证堆性质:通过从底层子树开始调整,确保每棵子树都是堆,最终整个树满足堆的性质,保证后续
top()
等操作能正确获取堆顶元素。
2.思路2:尾插法建堆(不建议)
//priority_queue类的成员函数push
void push(const T& x)
{
//将元素插入到底层容器末尾
_con.push_back(x);
//尾插后,对新插入的元素进行向上调整,恢复堆的性质
adjust_up(_con.size() - 1);
}
//迭代器区间构造函数(尾插法建堆)
template<class Iterator>
priority_queue(Iterator first, Iterator last)
: _con() //初始化为空容器
{
//遍历迭代器区间,逐个插入元素
while (first != last)
{
push(*first);
++firs
}
}
3.两种建堆方式对比
对比维度 | 向下调整法 | 尾插法(逐个插入) |
---|---|---|
时间复杂度 | O(n) | O(nlogn) |
核心逻辑 | 从最后一个非叶子结点开始,批量调整 | 逐个插入元素,每次向上调整 |
内存操作 | 一次性初始化底层容器,无频繁扩容 | 可能触发多次扩容(如 vector 动态增长) |
代码复杂度 | 需手动实现循环和调整逻辑 | 依赖 push 接口,代码更简洁 |
适用场景 | 大规模数据建堆(效率优先) | 小规模数据或对时间要求不高的场景 |
size、empty、top
//注意:const对象、普通对象都可以调用以下const成员函数
//获取优先队列中元素的个数
//由于优先队列是基于底层容器c实现的,所以直接调用底层容器_con的size()函数来获取元素个数
//const修饰表示该函数不会修改对象的状态
size_t size() const
{
return _con.size();
}
//判断优先队列是否为空
//同样基于底层容器c的状态来判断,调用_con的empty()函数
//const修饰表示该函数不会修改对象的状态
bool empty() const
{
return _con.empty();
}
//获取优先队列的堆顶元素
//堆顶元素在底层容器中通常位于第一个位置,所以返回_con.front()
//用const T&类型返回,一方面可以避免不必要的拷贝,提高效率;另一方面,
//因为堆顶元素不允许被修改(修改堆顶元素可能会破坏堆的特性,
//比如破坏堆的有序性,使得优先队列无法按正确的优先级规则工作),
//所以通过const限定返回值为只读
const T& top() const
{
return _con.front();//return _con[0];
}
push
1.向上调整
(1)思路描述
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从最后一个孩子开始的向上调整算法可以把完全二叉树调整成一个堆。向上调整算法有一个前提:除了最后一个孩子,其他结点组成的完全二叉树必须是个堆,才能调整。
int array[] = {70,56,30,25,15,10,100};
(2)代码实现
//向上调整函数,用于在优先队列中对新插入的元素进行向上调整以维护堆的性质
//注:仿函数的作用是通过控制比较规则来建立大堆或小堆
//函数对象com根据传入的仿函数类型决定比较方式
void adjust_up(int child)
{
//定义函数对象(仿函数)。作用:通过函数对象(仿函数)控制比较规则来建立大堆 或者 小堆。
Comapre com;
//找当前孩子child的父亲parent
int parent = (child - 1) / 2;
//注:最坏情况下,孩子来到堆顶位置即child = 0,则孩子不用继续向上调整,所以
//while循环的限制条件是child > 0.
while (child > 0)
{
//比较孩子和父亲大小关系的3种写法:
//写法1:直接使用运算符" < " (注:对自定义类型 T ,会调用 T 自己重载的operator<) 进行比较比较孩子和父亲的大小关系.
//if (_con[parent] < _con[child])//不建议这样写,因为这种写法只局限于建大堆时用,而且不能灵活切换比较规则,
//所以建议使用仿函数进行比较。
//写法2:(无需定义函数对象,而是定义函数匿名对象) 调用仿函数比较孩子和父亲的大小关系.
//if (Comapre()(_con[parent], _con[child]))//这种方式直接创建一个临时的仿函数对象并调用其operator()进行比较.
//写法3:(定义函数对象) 调用仿函数比较孩子和父亲的大小关系
//通过已经定义好的函数对象com调用operator()进行比较,这种方式较为常用,
//可以在函数开始时就确定好比较规则,便于代码的维护和理解.
if (com(_con[parent], _con[child]))
{
//根据仿函数的比较规则来确定堆的类型:
//如果使用less仿函数(默认情况),这里是大堆,若父亲比孩子要小,则父亲要往下换,孩子要往上调整。
//如果使用greater仿函数,则是小堆,此时若父亲比孩子大,则父亲要往下换,孩子要往上调整。
//孩子和自己父亲的值进行交换
swap(_con[child], _con[parent]);//调用std::swap进行交换
//交换完后,孩子来到自己父亲的位置
child = parent;
//重新给孩子找新的父亲
parent = (child - 1) / 2;
}
else
{
//若根据仿函数的比较规则,父亲比孩子大(对于大堆情况)或 父亲比孩子小(对于小堆情况),
//则孩子无需向上调整,直接使用break跳出循环来终止孩子向上调整。
break;
}
}
}
2.push
(1)思路描述
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。
注意:堆的性质遭到破坏指的是在堆尾插数据之后,该数据和之前堆中数据组成的完全二叉树不符合大/小堆的特性,则就通过向上调整方式把尾插数据调整到完全二叉树合适的位置,最终使得这个完全二叉树中所有元素符合大/小堆的特性,调整过后则此时的完全二叉树就是个堆。
(2)代码实现
//尾插函数,用于向优先队列中插入一个新元素
//函数将元素插入到优先队列底层容器的末尾,并通过调整操作维护堆的性质
void push(const T& x)
{
//尾插数据(注:这一步有可能破坏堆的特性,因为新插入的元素可能不满足堆序性质)
//将元素x插入到优先队列底层容器_con的末尾,这是利用了底层容器(如vector)的push_back操作
_con.push_back(x);
//把尾插的数据进行向上调整保持完全二叉树依然是个堆
//由于新插入的元素可能破坏了堆的性质,所以需要调用adjust_up函数对新插入元素进行向上调整
//传入新插入元素在底层容器中的下标(_con.size() - 1),使其在堆中找到合适的位置
adjust_up(_con.size() - 1);
}
pop
1.向下调整
(1)思路描述
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
(2)代码实现
//向下调整函数,用于在优先队列中对指定位置的元素进行向下调整以维护堆的性质
//注:仿函数的作用是通过控制比较规则来建立大堆或小堆
//函数对象com根据传入的仿函数类型决定比较方式
void adjust_down(int parent)
{
//找当前父亲左右孩子中最大孩子。默认左孩子是最大孩子child。
size_t child = parent * 2 + 1;//注:child表示当前父亲左右孩子中最大孩子。
while (child < _con.size())
{
//定义函数对象(仿函数)。作用:通过函数对象(仿函数)控制比较规则来建立大堆 或者 小堆。
Comapre com;
//验证左孩子是否是最大孩子,若不是则最大孩子child就是右孩子。
//注意:在判断右孩子是否是最大孩子之前,必须先判断当前父亲是否存在右孩子,即通过child + 1 < _con.size()判断。
//比较左右孩子大小关系的2种写法
//写法1:直接使用运算符" < " (注:对自定义类型 T ,会调用 T 自己重载的operator<) 进行比较比较左右孩子大小关系
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])//不建议这样写,因为这种写法只局限于建大堆时用(如果默认使用less仿函数),
//而且不能灵活切换比较规则,所以建议使用仿函数进行比较。
//写法2:(创建函数对象)调用仿函数比较左右孩子的大小关系
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))//调用operator()进行比较
{
//若右孩子存在,且右孩子大于左孩子,则右孩子就是最大孩子child,则此时需要child++使得child表示右孩子下标。
++child;
}
//比较父亲和自己最大孩子大小关系的2种写法
//写法1:直接使用运算符" < " (注:对自定义类型 T ,会调用 T 自己重载的operator<) 比较父亲和最大孩子的大小关系
//if (_con[parent] < _con[child])//不建议这样写,因为这种写法只局限于建大堆时用(如果默认使用less仿函数),
//而且不能灵活切换比较规则,所以建议使用仿函数进行比较。
//写法2:(创建函数对象)调用仿函数比较父亲和最大孩子的大小关系
if (com(_con[parent], _con[child]))//调用operator()进行比较
{
//根据仿函数的比较规则来确定堆的类型:
//如果使用less仿函数(默认情况),这里是大堆,若父亲比最大孩子要小,则孩子要往上换,父亲需往下调整。
//如果使用greater仿函数,则是小堆,此时若父亲比最大孩子大,则孩子要往上换,父亲需往下调整。
//父亲和最大孩子的值进行交换
swap(_con[child], _con[parent]);
//父亲来到孩子位置
parent = child;
//重新给父亲找新的最大孩子
child = parent * 2 + 1;//默认左孩子是最大孩子。
}
else//父亲比自己最大孩子要大或相等,则父亲就不用向下调整,直接使用break跳出循环来终止父亲向下调整。
{
//若根据仿函数的比较规则,父亲比最大孩子大(对于大堆情况)或 父亲比最大孩子小(对于小堆情况),
//则父亲就不用向下调整,直接使用break跳出循环来终止父亲向下调整。
break;
}
}
}
2.pop
(1)思路描述
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
注意:堆特性指的是大堆要满足双亲比左右孩子大/相等,而小堆要满足双亲比左右孩子小/相等。
(2)代码实现
//头删函数,用于删除优先队列(堆结构)中的堆顶元素
//该函数通过一系列操作,在删除堆顶元素后重新维护堆的性质
void pop()
{
//若堆为空就不能继续删除
if (empty())
return;
//交换堆顶和堆尾元素的值(注:这一步有可能破坏堆的特性,因为交换后堆顶元素可能不满足堆序性质)
//调用std::swap进行交换,将堆顶元素(下标为0)和堆尾元素(下标为_con.size() - 1)进行值交换
swap(_con[0], _con[_con.size() - 1]);// 第一个和最后一个交换
//交换后,删除此时的堆尾元素
//由于已经将需要删除的堆顶元素交换到了堆尾,所以使用pop_back操作删除堆尾元素,实现删除堆顶元素的目的
_con.pop_back();
//交换后,对此时的堆顶元素进行向下调整以保持完全二叉树依然是个堆
//交换后新的堆顶元素可能不满足堆的性质,因此调用adjust_down函数对其进行向下调整
//传入堆顶元素的下标0,使其在堆中找到合适的位置,重新维护堆的性质
adjust_down(0);
}
仿函数(类模板)
1.仿函数的作用
- 减少代码重复性:使用仿函数,只需编写一个
priority_queue
类模板,通过传入不同的仿函数(如less<T>
用于小堆,greater<T>
用于大堆)来改变堆的比较规则,避免大部分堆操作(如push
、pop
、adjust
等)的重复编写。 - 增强代码维护性:如果重载两个不同的
priority_queue
类模板来分别实现大堆和小堆,当需要修改堆的某些操作(如插入或删除)时,可能会出现遗漏或不一致的情况。而使用仿函数,只需在一个类模板中进行修改,因为比较规则的差异由仿函数来处理,从而降低了维护成本。 - 提高灵活性和扩展性:仿函数使
priority_queue
类模板更加灵活。除了less
和greater
这两个标准仿函数外,用户还可以自定义任意的比较规则仿函数,以适应各种复杂的优先级定义。
2.仿函数的基本概念
仿函数是一种行为类似函数的对象,通过在类中重载operator()
运算符实现。在使用时,仿函数对象的调用方式与普通函数相同,这使得它们可以像函数一样被传递和使用,为代码提供了更高的灵活性和复用性。
(1)less
仿函数的作用
- 定义小堆比较规则:
less
仿函数定义了小于关系的比较逻辑,即当x < y
时返回true
。在priority_queue
类模板中,当使用默认的比较器(即less<T>
)时,它会基于这个比较规则构建小堆。 - 在向上调整操作中的应用:在
adjust_up
函数(向上调整函数,用于插入元素后调整堆结构)中,less
仿函数用于判断当前节点(子节点)与其父节点的大小关系。如果父节点的值大于子节点的值(根据less
仿函数的比较规则),就需要交换它们的位置,直到堆满足小堆的性质,即每个父节点的值都小于或等于其子节点的值。 - 在向下调整操作中的应用:在
adjust_down
函数(向下调整函数,用于删除堆顶元素后调整堆结构)中,less
仿函数用于比较父节点与子节点的值,以及两个子节点之间的大小关系。如果父节点的值大于较小的子节点的值,就交换它们的位置,继续向下调整,以维护小堆的性质。
(2)greater
仿函数的作用
- 定义大堆比较规则:
greater
仿函数定义了大于关系的比较逻辑,即当x > y
时返回true
。当在priority_queue
类模板的实例化中显式使用greater<T>
作为比较器时,会基于这个比较规则构建大堆。 - 在向上调整操作中的应用:在
adjust_up
函数中,greater
仿函数用于判断当前节点(子节点)与其父节点的大小关系。如果父节点的值小于子节点的值(根据greater
仿函数的比较规则),就需要交换它们的位置,直至堆满足大堆的性质,即每个父节点的值都大于或等于其子节点的值。 - 在向下调整操作中的应用:在
adjust_down
函数中,greater
仿函数用于比较父节点与子节点的值,以及两个子节点之间的大小关系。如果父节点的值小于较大的子节点的值,就交换它们的位置,持续向下调整,以保持大堆的性质。
(3)总结
通过使用 less
和 greater
这两个仿函数,priority_queue
类模板可以根据不同的比较规则灵活地构建小堆或大堆。这种设计使得优先级队列的实现更加通用,用户可以根据具体的需求选择合适的比较器,从而满足不同场景下对元素优先级排序的要求。
3.priority_queue的默认仿函数类模板实现
//仿函数在priority_queue的作用:调用函数对象(仿函数)来控制比较规则进而建立大堆 或者 小堆。
//仿函数(注;仿函数的本质是对象,只是仿函数通过重载operator()可以让对象像函数一样使用)
//函数对象类模板
//比较规则:小于 (建大堆)
template<class T>
struct less
{
//使用传引用传参可以减少拷贝。由于只是比较x与y的大小关系,所以为了防止x与y被修改,
//则x与y统一使用const修饰。
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
//函数对象类模板
//比较规则:大于 (建小堆)
template<class T>
struct greater
{
//使用传引用传参可以减少拷贝。由于只是比较x与y的大小关系,所以为了防止x与y被修改,
//则x与y统一使用const修饰。
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
4.priority_queue的自定义仿函数类模板实现
场景 | 要求 | 示例代码 |
---|---|---|
T 为自定义类型 | 必须重载 operator< 和 operator> | class Date { bool operator<(const Date&) const; } |
T 为自定义类型地址 | 必须提供自定义仿函数,通过解引用指针比较对象内容 | class PDateLess { bool operator()(const Date*, const Date*) const; } |
改变堆类型 (大堆→小堆) | 通过模板参数 Compare 指定仿函数(如 greater<T> ) | priority_queue<int, vector<int>, greater<int>> pq; |
注意事项:
- 模板类型参数 T 为priority_queue底层容器存储的数据类型。
- T 为自定义类型:需在自定义类型中重载 operator< 或 operator>,供 priority_queue 的默认仿函数(less/greater)调用,以确定堆的大小关系。例如 Date 类重载相关运算符后,priority_queue<Date> 可直接按自定义逻辑建堆。
- T 为自定义类型地址:若直接使用默认仿函数,priority_queue 会比较地址值大小,而非地址指向的对象内容。因此需自定义仿函数(如 PDateLess/PDateGreater),在仿函数中解引用地址(*p1/*p2),调用自定义类型的比较运算符,确保按对象内容建堆。
测试:
- ①使用默认仿函数比较
- ②使用自定义仿函数比较
priority_queue类模板模拟实现的整个工程
//priority_queue.h
#include<iostream>
#include<vector>
using namespace std;
namespace stl
{
//默认仿函数类模板
//小于(大堆)
template<class T>
struct less
{
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
//大于(小堆)
template<class T>
struct greater
{
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
//priority_queue(堆)类模板
template<class T, class Container = vector<T>, class Comapre = less<T>>
class priority_queue
{
public:
//向上调整
void adjust_up(int child)
{
Comapre com;
int parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[parent] < _con[child])//不建议写这个,因为该比较规则只适用大堆
//if (Comapre()(_con[parent], _con[child]))//建议使用仿函数比较,适用大堆 或 小堆
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//向下调整
void adjust_down(int parent)
{
size_t child = parent * 2 + 1;
while (child < _con.size())
{
Comapre com;
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])//不建议写这个,因为该比较规则只适用大堆
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))//建议使用仿函数比较,适用大堆 或 小堆
{
++child;
}
//if (_con[parent] < _con[child])//不建议写这个,因为该比较规则只适用大堆
if (com(_con[parent], _con[child]))//建议使用仿函数比较,适用大堆 或 小堆
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//默认构造函数
priority_queue()
: _con()
{}
//迭代器区间构造
template<class Iterator>
priority_queue(Iterator first, Iterator last)
: _con(first, last)
{
int size = _con.size();
for (int parent = (size - 1 - 1) / 2; parent >= 0; parent--)
AdjustDown(parent);
}
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
void pop()
{
if (empty())
return;
swap(_con[0], _con[_con.size() - 1]);//swap(_con.front(), _con.back());
_con.pop_back();
adjust_down(0);
}
const T& top() const
{
return _con[0];//return _con.front();
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
private:
Container _con;
};
void test_priority_queue()
{
//1.迭代器区间构造(建堆)
/*vector<int> v{ 5,1,4,2,3,6 };
priority_queue<int, vector<int>, greater<int>> pq(v.begin(), v.end());*/
//2.逐渐尾插建堆
//2.1.测试底层封装vector<T>来建堆
//priority_queue<int, vector<int>, greater<int>> pq;//小堆
priority_queue<int> pq;//默认大堆
//2.2.测试底层封装deque<T>来建堆
//priority_queue<int, deque<int>> pq;//大堆
pq.push(1);
pq.push(0);
pq.push(5);
pq.push(2);
pq.push(1);
pq.push(7);
//边遍历边出数据
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
}
//Test.cpp
#include"priority_queue.h"
int main()
{
stl::test_priority_queue();
return 0;
}