当前位置: 首页 > news >正文

仿函数+优先级队列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::greaterstd::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::greaterstd::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> 作为比较器,但实际生成的是大根堆(降序)

具体表现:

  1. 队首元素(top())总是当前队列中的最大值
  2. 新元素插入时会被调整到合适位置,保持堆性质
  3. 出队(pop())总是移除当前最大值,出对顺序从大到小、
(2)原因:堆的构造逻辑

priority_queue 的底层容器默认是 vector,并使用堆算法维护。其比较函数的语义是:

比较器返回 true 时,表示第1个参数应排在第2个参数的后面(即优先级更低)。

在堆的调整过程中,(如 push_heap、pop_heap),用比较器判断父子节点关系:

  • 默认 std::less 表示:若父节点小于子节点,则需要调整(交换),最终保证父节点大于子节点 -> 大堆
  • 默认 std::greater 表示:若父节点大于子节点,则需要调整(交换),最终保证父节点小于子节点 -> 小堆
(3)排序方向总结
容器/算法默认比较器排序结果原因
std::sortstd::less升序(小 ->大)比较器定义"小于",排序时小的在前
std::sortstd::greater降序(大 ->小)比较器定义"大于",排序时大的在前
priority_queuestd::less降序(大堆)父节点必须大于子节点( less 比较时,小值优先级低)
priority_queuestd::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;}}}
  1. 对于给定的子序列索引child,其父节点的索引计算为:(child - 1) / 2
  2. 循环条件:while (child > 0) 循环确保我们不会尝试移动根节点(因为根节点的索引值为0,没有父节点)。循环继续执行,只要当前节点的索引值大于0。
  3. 完成交换后,更新child变量为原父节点的索引,因为交换后当前元素已经移动到了父节点的位置。下一步,对新的child值重新计算parent索引,继续执行可能的进一步交换。
  4. 循环终止条件:如果当前节点的值不小于其父节点的值(即堆的性质得到了满足),循环终止,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;}}}
  1. 向下调整算法,传递过来父节点parent,子节点 child = parent * 2 + 1;
  2. 默认左孩子比右孩子大(建大堆),若右孩子存在并且右孩子大于左孩子,++child;
  3. 循环条件:while (child < _con.size()),若子节点大于父节点,交换;同时更新新一轮的 parent 和 child。
  4. 当父节点大于子节点时,循环结束;或者当 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,希望看完这篇文章对友友们有所帮助!!!

点赞收藏加关注!!!

谢谢大家!!!

http://www.dtcms.com/a/303971.html

相关文章:

  • P2910 [USACO08OPEN] Clear And Present Danger S
  • AutoGen Agent 使用指南
  • 华为HCIA-Cloud Service 从知识到实践的学习指南
  • SQL排查、分析海量数据以及锁机制
  • WebRTC(十四):WebRTC源码编译与管理
  • Webpack 优化策略
  • 如何用即构ZEGO SDK和uni-app开发一款直播带货应用?
  • uniapp 如果进入页面输入框自动聚焦,此时快速返回页面或者跳转到下一个页面,输入法顶上来的页面出现半屏的黑屏问题。
  • 使用JavaScript实现轮播图的任意页码切换和丝滑衔接切换效果
  • uniapp 实现全局变量
  • 【数据结构】用堆实现排序
  • vue3+vite 使用liveplayer加载视频
  • MySQL MVCC:并发神器背后的原理解析
  • 网工知识——OSPF摘要知识
  • [工具类] 分片上传下载,MD5校验
  • echarts饼图
  • 封装$.ajax
  • 一个人开发一个App(数据库)
  • OpenAI Python API 完全指南:从入门到实战
  • 【学习笔记】Lean4 定理证明 ing
  • 7.29错题(zz)史纲 7章 建立新中国
  • Scala实用编程(附电子书资料)
  • Node.js 内置模块
  • AR辅助前端设计:虚实融合场景下的设备维修指引界面开发实践
  • 学习Scala语言的最佳实践有哪些?
  • GCC、glibc、GNU C(gnuc)的关系
  • SkSurface---像素的容器:表面
  • PowerShell脚本自动卸载SQL Server 2025和 SSMS
  • 零基础-动手学深度学习-7.7 稠密连接网络(DenseNet)
  • 景区负氧离子环境监测系统云平台方案