【C++STL】priority_queue 模拟实现与仿函数实战

前言:上一篇文章讲解了栈与队列的基本应用和实现方式,同时探讨了deque的操作原理与底层机制。本文将重点介绍另一种重要数据结构------优先级队列(priority_queue)。

🌟 专注用 图文结合拆解难点 + 代码落地知识,让技术学习从「难懂」变 "一看就会"!

🏠 个人主页MSTcheng · CSDN

💻 代码仓库MSTcheng · Gitee

💬 座右铭 : "路虽远行则将至,事虽难做则必成!"

文章目录

  • 一、priority_queue优先级队列的认识和使用
    • [1.1 优先级队列的介绍](#1.1 优先级队列的介绍)
    • [1.2 优先级队列的使用](#1.2 优先级队列的使用)
  • 二、优先级队列的模拟实现
    • [2.1 优先级队列的整体框架](#2.1 优先级队列的整体框架)
    • [2.2 priority_queue的默认构造](#2.2 priority_queue的默认构造)
    • [2.3 迭代器区间构造](#2.3 迭代器区间构造)
    • [2.4 push接口](#2.4 push接口)
    • [2.5 pop接口](#2.5 pop接口)
    • [2.6 empty判空](#2.6 empty判空)
    • [2.7 top取堆顶](#2.7 top取堆顶)
    • [2.8 size返回容器有效数据个数](#2.8 size返回容器有效数据个数)
  • 三、仿函数
  • 四、优先级队列总结

一、priority_queue优先级队列的认识和使用

1.1 优先级队列的介绍

💡优先级队列的概念

优先级队列 👉是一种类似于堆数据结构 ,其元素按照优先级排序,而非严格的先进先出(FIFO)或后进先出(LIFO)规则。每次从队列中取出的元素是当前优先级最高的(或最低的,取决于是大堆还是小堆)。

1.2 优先级队列的使用

priority_queue 核心接口表

方法/构造函数 描述
priority_queue() 构造一个空的优先级队列
priority_queue(first, last) 用迭代器范围 [first, last) 的元素构造优先级队列
empty() 检测队列是否为空,返回布尔值(true 为空,false 非空)
top() 返回队列中优先级最高的元素(堆顶元素),不删除
push(x) 向队列中插入元素 x
pop() 删除队列中优先级最高的元素(堆顶元素)

注:最大/最小元素的排序规则取决于队列的模板参数和比较器配置。

代码示例:

cpp 复制代码
#include<iostream>
#include<queue>
using namespace std;
int main()
{
	// 默认是大的优先级高 即大堆
	priority_queue<int> pq;
	// 控制小的优先级高 需要传一个 仿函数->后面会介绍
	//riority_queue<int,Greater<int>> pq;
	pq.push(4);
	pq.push(1);
	pq.push(6);
	pq.push(9);
	pq.push(4);
//通过循环取堆顶数据出堆顶数据来获得有序序列
	while (!pq.empty())
	{
		cout << pq.top() << " ";
		pq.pop();
	}
	cout << endl;
}

二、优先级队列的模拟实现

2.1 优先级队列的整体框架

cpp 复制代码
//仿函数/函数对象
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;
	}
};



//封装在一个命名空间里防止冲突
namespace my_priority_queue
{
	//容器适配器选择vecotr                   增加一个模板参数传仿函数 后面介绍     
	template<class T,class Container=vector<T>,class compare=Less<T>>
	class priority_queue
	{
	public:
	
	private:
		Container _con;
	};
}

📢容器适配器中的栈和队列默认使用deque作为底层容器,而优先级队列则选用vector这是因为优先级队列需要频繁进行下标访问操作,而vector在这方面比deque更具性能优势。

2.2 priority_queue的默认构造

由于优先级队列的底层使用的也是容器适配器,所以优先级队列的构造会调用底层适配容器的构造所以可以不用显示写构造,直接让编译器帮我们生成一个默认构造。

cpp 复制代码
	//让编译器强制生成默认构造
	priority_queue() = default;

2.3 迭代器区间构造

迭代器区间构造实际上也是给底层容器传了一个迭代器区间,只不过对于这段迭代器区间的值,要对他进行建堆。因为优先级队列的数据结构是一个堆的结构!底层存储数据的容器是vector!

cpp 复制代码
//使用迭代器区间构造
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
	:_con(first,last)//给底层的容器适配器传一个迭代器区间
{
	//_con.size()-1是下标 再减1就是最后一个数据 叶子结点 最坏的情况调整到叶子节点
	//adjust_down传的是父亲所以要除二
	for (int i = _con.size() - 1 - 1/2;i >= 0;i--)//这里不能使用size_t 使用size_t条件永远为真
	{
		//采用向下调整建堆 效率比较高
		adjust_down(i);
	}
}

👁️注意:使用向下调整建堆是因为向下调整建堆O(n)的效率高于向上调整建堆O(logn)。

对于建堆等操作还不熟悉的可以去看我之前的文章:堆和二叉树 这篇文章对于数据结构堆有非常详细的讲解

2.4 push接口

🤔回顾一下在学习数据结构堆的时候,无论是大堆还是小堆,在每次的插入后必须进行向上调整以保证堆的性质。

cpp 复制代码
//给外部使用的接口
void push(const T& x)
{
	//往尾部插入数据 调用底层容器的尾插
	_con.push_back(x);
	//向上调整 
	adjust_up(_con.size()-1);
}
//向上调整算法不让外面调用限定为私有
private:
	void adjust_up(size_t child)
	{
		compare com;//传一个仿函数对象
		
		size_t parent = (child - 1) / 2;
		//最坏的情况就是一直调整到根 最终parent的越界 给child所以判断条件为child>0
			while (child>0)
		{
			//大于建小堆
			//if (_con[child] > _con[parent])
			//小于建大堆 默认
			if (_con[child] < _con[parent])
			//if(com(_con[parent],_con[child]))//这里要模仿小于所以要将child和parent调换一下位置  这里使用仿函数后面会介绍
			{
				std::swap(_con[child], _con[parent]);
				//继续向上走
				child = parent;
				parent = (child - 1) / 2;
			}
			else
			{
				break;
			}
			
		}
	}

这里的向上调整算法与数据结构中堆的向上调整算法原理相同,但额外增加了仿函数的判断逻辑。关于这一部分的具体实现细节,我们将在后续内容中详细介绍。

2.5 pop接口

堆结构中删除优先级最高元素时,不能直接移动数据,否则会破坏堆结构并需要重新建堆。 正确做法是:

  1. 交换堆顶与尾部元素
  2. 删除尾部元素
  3. 对堆顶元素执行向下调整
cpp 复制代码
void pop()
{
	//先将最后一个位置的数据与堆顶交换再删除
	std::swap(_con[0], _con[_con.size() - 1]);
	_con.pop_back();//调用底层容器的尾删

	//向下调整 
	adjust_down(0);
}
private:
	void adjust_down(size_t parent)
	{

	compare com;//传入仿函数对象

	size_t child = parent * 2 + 1;
	//最坏的情况就是调整到根节点 所以child会越界 
	while (child<_con.size())
	{
		//找出大的那个孩子
		//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
		if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))

		{
			child++;
		}
		//再判断父子关系 默认建大堆
		//if (_con[child] > _con[parent])
		if (com(_con[parent],_con[child]))
		{
			std::swap(_con[child], _con[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}

2.6 empty判空

判空直接调用底层容器的判空接口。

cpp 复制代码
//底层调用vector的判空接口
bool empty()
{
	return _con.empty();
}

2.7 top取堆顶

取堆顶👉直接调用底层容器的0下标处的数据,因为0下标处的数据就是优先级最高的元素,但不会删除数据。

cpp 复制代码
const T& top()
{
	return _con[0];
}

2.8 size返回容器有效数据个数

由于底层容器使用的是vector,所以直接调用vector的size接口即可。

cpp 复制代码
size_t size()
{
	return _con.size();
}

三、仿函数

3.1仿函数的简单认识🆕

仿函数(Functor)是一种行为类似函数的对象,通过重载operator()实现。它可以是类或结构体实例,能够像普通函数一样被调用,同时具有对象的特性(如状态存储)。

3.2 仿函数的使用

cpp 复制代码
//定义一个比较小于的类
template<class T>
class Less
{
public:
	bool operator()(const T& x, const T& y)
	{
		return x < y;
	}
};
int main()
{
//使用该类定义一个lessFunc 
	Less<int> lessFunc;
	cout << lessFunc(1, 2) << endl;//可以像调用函数一样调用
	cout << lessFunc.operator()(1, 2) << endl;//本质是调用operator()
}

从上述结果可以看出,仿函数本质上是一个定义了 operator() 运算符的类 。当实例化这个类时,其 operator() 能够返回 x 和 y 中的较小值。若需要实现比较取大值的功能,只需定义另一个类并重载 operator(),使其返回 x 和 y 中的较大值即可。

再回过头来看看我们之前在模拟实现部分看到的二处有关仿函数的修改:

第一处:

cpp 复制代码
template<class T,class Container=vector<T>,class compare=Less<T>>
	class priority_queue
	{
	};

在框架设计中,我们首先添加了一个模板参数,其默认值设置为"小于"比较的仿函数。因此,后续通过该compare参数创建的对象默认会执行小于比较的逻辑。

第二处:

cpp 复制代码
void adjust_up(size_t child)
{
		compare com;//定义一个仿函数对象
		while (child>0)
		{
			if(com(_con[parent],_con[child]))
			//com(_con[parent],_con[child])就相当于上面 lessFunc(1, 2)这样调用
		}
}

void adjust_down(size_t parent)
{

	compare com;//定义一个仿函数对象

	while (child<_con.size())
	{
		if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
		
		if (com(_con[parent],_con[child]))

}

在堆的向上调和向下调整算法中,我们首先创建了一个仿函数对象com。默认情况下,com执行的是小于比较逻辑 。通过在if语句传递不同的仿函数对象 ,我们可以灵活地控制比较逻辑类型:传递Less仿函数执行小于比较,传递Greater仿函数则执行大于比较。👍

cpp 复制代码
int main()
{
//下面使用vector的排序来举例仿函数的作用 
	vector<int> v = { 3,2,6,1,7 };
	// < 升序 默认情况下是Less小于
	sort(v.begin(), v.end(),Less<int>());

	// > 降序 传一个比较器greater大于
	sort(v.begin(), v.end(), greater<int>());
	//sort(v.rbegin(), v.rend());//传反向迭代器也能得到类似的效果 即将升序调成降序

	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

😰需要注意的是由于 Less 类和 greater 类都是类模板 ,因此在传参时必须指定类型参数。必须写成 Less<int>() 的形式,而不能直接使用 Less()。只有在函数模板的时候传递的才是类对象。

3.3 仿函数的其他用法

实际上,仿函数的功能远比简单的比较操作更丰富。下面通过具体代码示例展示其多样化的应用场景:

cpp 复制代码
struct OP1
{
	bool operator()(int x)
	{
		return x % 2 == 0;
	}
};

struct OP2
{
	int operator()(int x)
	{
		if (x % 2 == 0)
			return x * 2;
		else
			return x;
	}
};

int main()
{
	int a[] = { 1,2,3,2,5,7,1 };

	// 查找第一个偶数 find_if是算法库的函数
	auto it = find_if(a, a + 7, OP1());//传递仿函数 这里传的是匿名对象
	cout << *it << endl;

	vector<int> v = { 1,2,3,4,5,6,7 };
	// 偶数值*2  
	transform(v.begin(), v.end(), v.begin(), OP2());
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

四、优先级队列总结

💡核心特性

优先级排序:每个元素关联一个优先级值,通常为数字。高优先级元素先出队。

动态操作:支持插入新元素(enqueue)和删除最高优先级元素(dequeue),可能伴随调整内部结构。

实现灵活性:可用堆、二叉搜索树或数组等实现,不同实现影响操作的时间复杂度。

💡容器适配器

优先级队列的适配容器:优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。

💡应用场景

任务调度:操作系统中的进程优先级管理。 路径搜索算法:如Dijkstra算法中优先处理最短路径节点。

数据流处理:实时系统中优先处理高重要性事件。

相关推荐
还有几根头发呀1 小时前
从 C++ 的角度,系统地解释 进程(Process)、线程(Thread)、协程(Coroutine) 的概念、原理、优缺点,以及常见应用场景。
c++
oioihoii1 小时前
Python与C++:从哲学到细节的全面对比
c++
小年糕是糕手1 小时前
【C++】C++入门 -- inline、nullptr
linux·开发语言·jvm·数据结构·c++·算法·排序算法
郝学胜-神的一滴1 小时前
Python中一切皆对象:深入理解Python的对象模型
开发语言·python·程序人生·个人开发
kk哥88991 小时前
Keil MDK 5.39 编程 + 调试 ,ARM 嵌入式开发!如何安装
c++·arm
重启的码农2 小时前
enet源码解析 (2) 对等节点 (ENetPeer)
c++·网络协议
csbysj20202 小时前
JSP 隐式对象
开发语言
星期天22 小时前
3.2联合体和枚举enum,还有动态内存malloc,free,calloc,realloc
c语言·开发语言·算法·联合体·动态内存·初学者入门·枚举enum
塞北山巅2 小时前
camera hal层(AF)
c++·camera