仿函数+优先级队列priority_queue的模拟实现
片头
上一篇中,我们对优先级队列有了一个大致的了解,这一篇中,我们将对优先级队列priority_queue 进行模拟实现,咱们开始咯~
一、回顾优先级队列priority+queue
优先级队列默认使用vector作为其底层存储数据的容器,
在vector上又使用了堆算法将vector中的元素构造成堆的结构,
因此,priority_queue 就是堆,所有需要用到堆的位置,都可以考虑使用 priority_queue。
注意:默认情况下,priority_queue 是大堆。
1.1 函数使用
(1)构造函数
有关参数的使用我们后文进行详细讲解,创建一个优先级队列:
priority_queue<int> q;
(2)empty() 函数
检测优先级队列是否为空,是返回true,否则返回false
(3)top() 函数
返回优先级队列中最大(最小元素),即堆顶元素
(4)push() 函数
在优先级队列中插入元素x
(5)pop() 函数
删除优先级队列中最大(最小)元素,即堆顶元素
测试一下:
void test(){priority_queue<int> q;q.push(3);q.push(1);q.push(5);q.push(2);q.push(4);while (!q.empty()){cout << q.top() << " ";q.pop();}cout << endl;}
我们按照不同顺序插入,来观察它的顶端元素结果:
默认情况下,priority_queue是大堆。
那如何构建小堆呢?这里就涉及到仿函数
1.2 仿函数的使用和介绍
在C++的std::priority_queue的实现中,默认情况下,优先级是用元素之间的小于操作来判定的,即元素越大,优先级越高。
模板参数解释如下:
1、class Container = vector<T> :
这是用来内部存储队列中元素的容器类型。默认是 std::vector,但也可以是其他符合要求的容器。比如:std::deque。有一点要注意的是:必须支持随机访问迭代器(Random Access Iterator),以及 front() ,push_back(),pop_back() 的操作。
2、class Compare = less<typename Container::value_type>:
这是用来比较元素优先级的比较函数对象。默认是 std::less,该函数使得最大的元素被认为是最高优先级(形成大根堆)。如果想要最小的元素为最高优先级(形成小根堆),可以通过提供 std::greater 函数对象作为这个模板参数来改变这个行为。
默认使用 less 这个仿函数,如果我们需要建立小根堆,需要自己传参:
priority_queue<int,vector<int>,greater<int>> q;
我们接下来详细讲解一下什么是仿函数
在C++中,仿函数是一种使用对象来模拟函数的技术。它们通常是通过类来实现的,该类重载了函数调用操作符(operator( ))。仿函数可以像普通函数一样被调用,但它们可以拥有状态(即,它们可以包含成员变量,继承自其他类等。
下面是使用仿函数的一个简单例子:
#include<iostream>
using namespace std;#include<queue>
#include<functional>namespace bit
{//定义一个仿函数类class Add{public://构造函数, 可以用来初始化内部状态, 这里没有使用Add(){}//重载函数调用操作符int operator()(int a, int b){return a + b;}};
}int main()
{//创建一个仿函数对象bit::Add add_func;//使用仿函数对象cout << add_func(10, 5) << endl;cout << add_func.operator()(10, 5) << endl;cout << bit::Add()(10, 5) << endl;return 0;
}
在这个例子中,我们定义了一个名为 Add 的仿函数类,它重载了 operator()来实现两数相加的功能。然后在 main 函数中创建了该类的一个实例 add_func 并且像调用函数一样使用 add_func(10,5) 来求和。
Add()(10, 5)使用了匿名对象
仿函数广泛用于C++标准库中,特别是在算法(std::sort,std::for_each等)作为比较函数或者操作函数,以及在容器(如:std::set 或者 std::map)中作为排序准则。
这是如何在 std::sort 算法中使用仿函数的一个实例:
#include<iostream>
using namespace std;
#include<algorithm>
#include<vector>namespace bit
{class Compare{public:bool operator()(int a, int b){return a > b; //降序排列}};
}int main()
{vector<int> v = { 2,4,1,3,5 };//使用仿函数对象sort(v.begin(), v.end(),bit::Compare());for (auto e : v){cout << e << " ";}//输出: 5 4 3 2 1return 0;
}
在上面的例子中,Compare 仿函数用来定义一个降序规则,随后在 std::sort 中将其进行实例化并传递给算法进行降序排序
仿函数的一个主要优点是它们可以保持状态,这意味着它们可以在多次调用之间保存和修改信息。这使得它们非常灵活和强大。此外,由于它们是类的实例,它们也可以拥有额外的方法和属性
1.3 greater 和 less
std::greater 和 std::less 是预定义的函数对象模板,用于执行比较操作,它们定义在<functional> 头文件中。std::greater 用于执行大于 (>) 的比较,而 std::less 用于执行小于 (<) 的比较。
以下是 std::less 和 std::greater 的典型用法:
int main()
{vector<int> v = { 5,2,4,3,1 };//使用std::less来升序排序sort(v.begin(), v.end(), less<int>());for (auto e : v){cout << e << " ";}cout << endl;//使用std::greater来降序排序sort(v.begin(), v.end(), greater<int>());for (auto e : v){cout << e << " ";}cout << endl;return 0;
}
函数对象模板 std::less 和 std::greater 的实现通常如下:
namespace STD {template<class T>struct less{bool operator()(const T& lhs, const T& rhs) const {return lhs < rhs;}};template<class T>struct greater{bool operator()(const T& lhs, const T& rhs) const{return lhs > rhs;}};
}
在C++11之后的版本中,由于引入了泛型 lambda 表达式,直接传递 lambda 函数给标准算法(如:std::sort),使得使用 std::greater 和 std::less 变得不那么必要了。以下是使用 lambda 表达式的例子:
#include<iostream>
using namespace std;
#include<algorithm>
#include<vector>int main()
{vector<int> v = { 2,4,1,3,5 };//使用lambda表达式作为比较函数进行升序排序sort(v.begin(), v.end(), [](int a, int b) {return a < b; });for (auto e : v){cout << e << " ";}cout << endl;//使用lambda表达式作为比较函数进行降序排序sort(v.begin(), v.end(), [](int a, int b) {return a > b; });for (auto e : v){cout << e << " ";}cout << endl;return 0;
}
来看看这里的参数传递
vector<int> v = { 2,4,1,3,5 };//使用迭代器区间构造优先级队列pq,排升序priority_queue<int, vector<int>, greater<int>> pq(v.begin(), v.end());//使用sort排降序sort(v.begin(), v.end(), greater<int>());
priority_queue 传的是一个类型,而 sort 需要传递对象,我们这里传递的是匿名对象。
1.4 理解 priority_queue 底层逻辑
Q:那为什么同样传递 less<int> 或者 greater<int> ,sort 的排序方式和 priority_queue 的排序方式恰好相反呢?
A:这源于 priority_queue 的底层机制是堆(Heap)。
(1)priority_queue 的默认行为
priority_queue 默认使用 std::less<T> 作为比较器,但实际生成的是大根堆(降序)
具体表现:
- 队首元素(top())总是当前队列中的最大值
- 新元素插入时会被调整到合适位置,保持堆性质
- 出队(pop())总是移除当前最大值,出对顺序从大到小、
(2)原因:堆的构造逻辑
priority_queue 的底层容器默认是 vector,并使用堆算法维护。其比较函数的语义是:
比较器返回 true 时,表示第1个参数应排在第2个参数的后面(即优先级更低)。
在堆的调整过程中,(如 push_heap、pop_heap),用比较器判断父子节点关系:
- 默认 std::less 表示:若父节点小于子节点,则需要调整(交换),最终保证父节点大于子节点 -> 大堆。
- 默认 std::greater 表示:若父节点大于子节点,则需要调整(交换),最终保证父节点小于子节点 -> 小堆。
(3)排序方向总结
容器/算法 | 默认比较器 | 排序结果 | 原因 |
std::sort | std::less | 升序(小 ->大) | 比较器定义"小于",排序时小的在前 |
std::sort | std::greater | 降序(大 ->小) | 比较器定义"大于",排序时大的在前 |
priority_queue | std::less | 降序(大堆) | 父节点必须大于子节点( less 比较时,小值优先级低) |
priority_queue | std::greater | 升序(小堆) | 父节点必须小于子节点( greater 比较时,大值优先级低) |
(4)如何改变 priority_queue 的排序方向?
若需实现小堆(升序)-> 队首为最小值,需显示指定:
//小堆(升序):最小值在top()priority_queue<int, std::vector<int>, std::greater<int>> min_heap;
(5)底层原理解析
以 std::less 为例,在堆调整中的逻辑:
//伪代码: 堆下沉操作while (父节点 < 子节点) //使用 less: 父 < 子 -> 需要交换{swap(父, 子);}
- 最终保证:父节点 >= 子节点 -> 大堆
而用 std::greater 时:
while (父节点 > 子节点) //使用 greater: 父 > 子 -> 需要交换{swap(父, 子);}
- 最终保证:父节点 <= 子节点 -> 小堆
(6)记忆技巧
priority_queue 的比较器定义的是"优先级低"的条件:
- less -> 小的值"优先级低" -> 大的值优先 -> 大堆(降序)
- greater -> 大的值"优先级低" -> 小的值优先 -> 小堆(升序)
总结:
比较器 | 在sort( )中的行为 | 在priority_queue中的行为 | 底层逻辑 |
std::less | 升序(小->大) | 大堆(队首最大,降序出队) | 定义"优先级低"的条件(值小的优先级低) |
std::greater | 降序(大->小) | 小堆(队首最小,升序出队) | 定义"优先级低"的条件(值大的优先级低) |
关键区别:比较器的含义
在priority_queue中,比较器不是直接定义排序顺序,而是定义"优先级"
//伪代码: 堆调整逻辑if (comp(parent, child)){//当父节点"优先级低于"子节点时交换swap(parent, child);}
- 当使用 std::less 时,comp(a, b) = a < b -> 若父节点小于子节点,说明父节点优先级低 -> 需要交换 -> 最终形成大根堆
- 当使用 std::greater时,comp(a, b) = a > b -> 若父节点大于子节点,说明父节点优先级低 -> 需要交换 -> 最终形成小根堆
实际使用实例:
#include<iostream>
using namespace std;
#include<queue>
#include<functional>
#include<algorithm>
#include<vector>int main()
{//默认大堆 (less -> 最大值在队首)priority_queue<int> max_heap;//等价于://priority_queue<int,vector<int>,less<int>> max_heap;max_heap.push(3);max_heap.push(1);max_heap.push(4);//出对顺序: 4 -> 3 -> 1 (降序)//小堆 (greater -> 最小值在队首)priority_queue<int, vector<int>, greater<int>> min_heap;min_heap.push(3);min_heap.push(1);min_heap.push(4);//出对顺序: 1 -> 3 -> 4 (升序)return 0;
}
二、priority_queue 的模拟实现
2.1 基本框架
基本框架如下:
#pragma once
#include<iostream>
using namespace std;#include<vector>
#include<list>namespace bit
{template<class T,class Container = vector<T>,class Compare = less<T>>class priority_queue{public:void adjust_up(size_t child){ }void push(const T& val){ }void adjust_down(size_t parent){ }void pop(){ }bool empty(){ }size_t size(){ }const T& top(){ }private:Container _con;};
}
它的底层是堆,我们就使用vector作为底层容器
咱们来先补充简单的接口
(1)push( )
优先级队列里面,我们要插入数据,会进行向上调整
实现如下:
void push(const T& val){ _con.push_back(val);adjust_up(_con.size() - 1);}
(2)pop( )
pop需要删除堆顶的数据,我们的方式是将堆顶和最后1个元素进行交换,也就是头尾交换,尾删,再向下调整
void pop(){swap(_con[0], _con[_con.size() - 1]); //首尾元素交换_con.pop_back(); //尾删adjust_down(0); //从第1个节点开始向下调整}
(3)empty( )
直接判断即可
bool empty(){ return _con.empty();}
(4)size( )
size_t size(){ return _con.size();}
(5)top( )
//获取堆顶元素const T& top(){ return _con[0];}
接下来我们来完成2个关键的函数,向上调整算法和向下调整算法
(6)adjust_up( )
当前位置每次和它的父节点比较
void adjust_up(size_t child){ size_t parent = (child - 1) / 2;while (child > 0){if (_con[parent] < _con[child]) {swap(_con[parent], _con[child]);child = parent;parent = (child - 1) / 2;}else {break;}}}
- 对于给定的子序列索引child,其父节点的索引计算为:(child - 1) / 2
- 循环条件:while (child > 0) 循环确保我们不会尝试移动根节点(因为根节点的索引值为0,没有父节点)。循环继续执行,只要当前节点的索引值大于0。
- 完成交换后,更新child变量为原父节点的索引,因为交换后当前元素已经移动到了父节点的位置。下一步,对新的child值重新计算parent索引,继续执行可能的进一步交换。
- 循环终止条件:如果当前节点的值不小于其父节点的值(即堆的性质得到了满足),循环终止,else break 执行。
(7)adjust_down( )
void adjust_down(size_t parent){size_t child = parent * 2 + 1;while (child < _con.size()){if (child + 1 < _con.size() && _con[child] < _con[child + 1]){child = child + 1;}if (_con[parent] < _con[child]){swap(_con[parent], _con[child]);parent = child;child = parent * 2 + 1;}else {break;}}}
- 向下调整算法,传递过来父节点parent,子节点 child = parent * 2 + 1;
- 默认左孩子比右孩子大(建大堆),若右孩子存在并且右孩子大于左孩子,++child;
- 循环条件:while (child < _con.size()),若子节点大于父节点,交换;同时更新新一轮的 parent 和 child。
- 当父节点大于子节点时,循环结束;或者当 child 走完最后1个叶子节点,循环结束。
2.2 两个函数的优化
上面实现的代码只能完成一种堆的实现,如何进行封装使我们能够根据传参实现大堆或者小堆呢?
这里就涉及到仿函数了,注意看我们模板中第3个参数:
template<class T,class Container = vector<T>,class Compare = less<T>>
咱们首先补充 greater 和 less 这2个类:
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;}};
我们控制大小堆,则需要控制2个adjust函数的比较逻辑
仿函数本质是一个类,可以通过模板参数进行传递,默认传的为 less,控制它为大堆
template<class T,class container = vector<T>,class Compare = less<T>>void adjust_up(size_t child){ Compare com;size_t parent = (child - 1) / 2;while (child > 0){//if(_con[child] > _con[parent])//if(_con[parent] < _con[child]if (com(_con[parent],_con[child]) {swap(_con[parent], _con[child]);child = parent;parent = (child - 1) / 2;}else {break;}}}
com是Compare的对象,它的对象可以像函数一样使用
template<class T,class Container = vector<T>,class Compare = less<T>>void adjust_down(size_t parent){Compare com;size_t child = parent * 2 + 1;while (child < _con.size()){//if (child + 1 < _con.size() && _con[child + 1] > _con[child])//if (child + 1 < _con.size() && _con[child] < _con[child + 1]if (child + 1 < _con.size() && com(_con[child],_con[child+1])){child = child + 1;}//if (_con[child] > _con[parent])//if (_con[parent] < _con[child])if (com(_con[parent],_con[child])){swap(_con[parent], _con[child]);parent = child;child = parent * 2 + 1;}else {break;}}}
2.3 对于自定义类型的其他仿函数使用
如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供 > 或 < 的重载
class Date{public:friend ostream& operator<<(ostream& _cout, const Date& d);Date(int year = 1900, int month = 5, int day = 4):_year(year),_month(month),_day(day){}bool operator<(const Date& d) const{return (_year < d._year)|| (_year == d._year && _month < d._month)|| (_year == d._year && _month == d._month && _day < d._day);}bool operator>(const Date& d) const{return (_year > d._year)|| (_year == d._year && _month > d._month)|| (_year == d._year && _month == d._month && _day > d._day);}private:int _year;int _month;int _day;};ostream& operator<<(ostream& _cout, const Date& d){cout << d._year << "-" << d._month << "-" << d._day << endl;return _cout;}
void test(){priority_queue<Date, vector<Date>, greater<Date>> pq;Date d1(2024, 4, 8);pq.push(d1);pq.push(Date(2024, 4, 10));pq.push({ 2024,2,15 });while (!pq.empty()){cout << pq.top() << " ";pq.pop();}cout << endl;}
输出结果:
再看看下面这个:如果我存的是指针呢?
void test5(){priority_queue<Date*, vector<Date*>, greater<Date*>> pqptr;pqptr.push(new Date(2024, 4, 14));pqptr.push(new Date(2024, 4, 11));pqptr.push(new Date(2024, 4, 15));while (!pqptr.empty()){cout << *(pqptr.top()) << " ";pqptr.pop();}cout << endl;}
我们发现2次运行的结果不一样,这是因为我们比较的是地址,而不是值,地址是new出来的,无法比较大小。
我们需要重新构造一个仿函数:
class GreaterPDate{public:bool operator()(const Date* p1, const Date* p2){return *p1 > *p2;}};
priority_queue<Date*, vector<Date*>, GreaterPDate> pqptr;
再看一个实际问题,如果我的一个结构体存储一个商品
struct Goods{string _name; //名字double _price; //价格int _evaluate; //评价Goods(const char* str, double price, int evaluate):_name(str),_price(price),_evaluate(evaluate){ }};
我们可以利用仿函数来实现对不同指标的排序
struct ComparePriceLess{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}};struct ComparePriceGreater{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}};struct CompareEvaluateLess{bool operator()(const Goods& gl, const Goods& gr){return gl._evaluate < gr._evaluate;}};struct CompareEvaluateGreater{bool operator()(const Goods& gl, const Goods& gr){return gl._evaluate > gr._evaluate;}};
int main()
{vector<Goods> v = {{"苹果",2.1,5},{"香蕉",3,4},{"橙子",2.2,3},{"菠萝",1.5,4}};sort(v.begin(), v.end(), bit::ComparePriceLess());sort(v.begin(), v.end(), bit::ComparePriceGreater());sort(v.begin(), v.end(), bit::CompareEvaluateLess());sort(v.begin(), v.end(), bit::CompareEvaluateGreater());return 0;
}
有了仿函数,我们就可以对这种自定义类型实现想要的排序。
片尾
今天我们学习了如何模拟实现priority_queue,希望看完这篇文章对友友们有所帮助!!!
求点赞收藏加关注!!!
谢谢大家!!!