C++:priority_queue模拟实现

C++:priority_queue模拟实现

    • 什么是priority_queue
    • 模拟实现
      • 向上调整算法
      • 向下调整算法
      • 插入与删除
    • 仿函数


什么是priority_queue

priority_queue称为优先级队列。优先级队列是一种特殊的队列,其中每个元素都有一个相关的优先级。元素的优先级决定了它们在队列中的顺序。具有较高优先级的元素在队列中被排在前面,而具有较低优先级的元素在队列中被排在后面。

可以简单理解为,priority_queue其实就是堆,如果作用于整型,可以让最大/最小的数字处于堆顶,而其也可以作用于其它类型,比如作用于string,那么可以根据字典序来排序。

接下来我带大家实现一下这个结构:


模拟实现

在STL中,preority_queue是一个容器适配器,因为其只要求了数据按照指定的顺序排列,但是没有对底层结构做要求,所以我们直接用其他容器做底层即可。

在此我使用了vector做底层,因为在优先级队列中,我们需要频繁地进行下标访问,使用deque也是可以的。

基本结构:

template <class T, class Container = vector<T>>
class preority_queue
{
private:
	Container _con;
};

模板参数:

T:这个优先级队列承载的元素类型
Container :底层容器类型,此处默认值为vector

我们看看preority_queue有哪些接口:
在这里插入图片描述
接下来我们一一实现:


top:
想要得到preority_queue优先级最高的元素,其实就是拿到vector的第一个元素_con[0]

const T& top()
{
	return _con[0];
}

size:
preority_queue的大小,就是vector的大小。

size_t size()
{
	return _con.size();
}

empty:
判断preority_queue是否为空,就是判断vector是否为空。

bool empty()
{
	return _con.empty();
}

向上调整算法

现在假设我有以下堆结构:
在这里插入图片描述

我现在在堆尾部插入一个数据,如何将数据调整到合适的位置,保证这个结构依然满足堆的要求?
在这里插入图片描述
想要将其插入到指定位置,就要使用到向上调整算法:将最后一个节点向上调整到合适的位置

首先讲解一个公式:堆结构中父节点与子节点的下标关系

假设父节点的下标为parent,则其左子节点的下标为 2 * parent+1,右子节点的下标为 2 * parent+2。

由于大堆要保证每隔父亲节点大于两个子节点,而除去最后一个节点,其它的节点已经满足堆结构了,所以此处需要将最后一个节点不断地与其父亲节点比较,如果其比父亲节点大,就交换位置,然后继续和新的父亲节点比较,直到比当前的父亲节点小,或者到达堆顶为止。

图示:
请添加图片描述

vector中的效果(实际效果):
请添加图片描述

代码实现:

void AdjustUp(int child)
{
	int parent = (child - 1) / 2;

	while (child > 0)
	{
		if (_con[child] > _con[parent])
		{
			Swap(_con[child], _con[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

代码详解:

  1. 定义父节点索引:
int parent = (child - 1) / 2;

根据完全二叉树的性质,节点i的父节点索引是(i - 1) / 2,所以计算出child节点的父节点索引。

  1. 进入循环:
while (child > 0)

child大于0时,继续执行循环。循环的目的是将child节点与其父节点进行比较,并根据需要进行交换。

  1. 比较child节点与其父节点的大小:
if (_con[child] < _con[parent])

如果child节点的值小于其父节点的值,说明需要进行交换。这是一个小堆的向上调整操作。如果想要实现大堆的向上调整,需要将判断条件改为>

  1. 交换节点值和更新索引:
Swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;

交换child节点和父节点的值,然后更新childparent的值,使其指向交换后的节点。

  1. 循环结束:
else
{
    break;
}

如果child节点大于等于其父节点,说明调整完成,跳出循环。

通过这个向上调整的操作,可以将新插入的元素调整到合适的位置,以保证堆的性质。


向下调整算法

如果堆中某个节点的值被修改,如何调整这个堆的结构保证其依然满足堆的要求?

当堆中的某个节点的值发生改变时(例如,该节点的值被修改),需要进行向下调整操作来保持堆的性质。

向下调整的基本思想是将当前节点与其子节点进行比较,并根据堆的类型(大堆或小堆)选择合适的交换操作。如果当前节点的值小于(或大于)其子节点的值,那么需要将当前节点与其子节点中的较大(或较小)值进行交换。然后,继续向下调整交换后的子节点,直到满足堆的性质为止。

示意图如下:
请添加图片描述vector中的效果(实际效果):
请添加图片描述

代码如下:

void AdjustDown(int parent)
{
	int child = parent * 2 + 1;

	while (child < _con.size())
	{
		if (child + 1 < con.size() && _con[child + 1] > _con[child])
		{
			child++;
		}

		if (_con[child] > _con[parent])
		{
			Swap(_con[child], _con[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

首先,计算待调整节点的左子节点下标child = parent * 2 + 1

然后,进入一个循环,判断child是否越界。如果child < size,则说明待调整节点有左子节点。

在循环中,首先判断是否存在右子节点,并且右子节点的值大于左子节点的值,如果满足这个条件,则将child更新为右子节点的下标。

接下来,判断child节点的值是否大于parent节点的值。如果满足这个条件,则交换childparent节点的值,并更新parentchild,再次计算child的值。

如果child节点的值不大于parent节点的值,则退出循环。

函数结束后,即可保证以parent节点为根的子树满足堆的性质。


插入与删除

push:
我们只需要将待插入元素放到末尾,然后将其向上调整即可:

void push(const T& x)
{
	_con.push_back(x);
	adjust_up(_con.size() - 1);
}

pop:
我们只需要将第一个元素与最后一个元素交换,然后删除最后一个元素,最后把刚刚交换上来的第一个元素向下调整即可:

void pop()
{
	swap(_con[0], _con[_con.size() - 1]);
	_con.pop_back();
	adjust_down(0);
}

但是至此我们的优先级队列还不是完全体,因为其只能固定是大堆/小堆,或者说优先级的比较方式是固定的,想要解决这个问题,我们就需要仿函数。


仿函数

仿函数本质上是一个类,它具有函数调用运算符(operator())的特性。仿函数的本质是一种可调用的对象,它可以像函数一样被调用。

我先为大家展示一个仿函数:

class Add 
{
    int operator()(int a, int b) 
    {
        return a + b;
    }
};

int main()
 {
    Add add;
    int result = add(3, 4); 
    
    return 0;
}

在上面的示例中,我们定义了一个名为Add的类,它重载了函数调用运算符operator(),并实现了对两个整数相加的逻辑。在主函数中,我们创建了一个Add的实例add,并通过调用函数调用运算符对两个整数进行相加。

add本质上明明是一个类的对象,但是由于重载了运算符operator(),所以可以模仿函数的行为,完成函数的调用。

那么我们为什么不直接写一个函数,完成两个数字的加法,而是要搞仿函数这样的东西呢?

这就不得不提到被人诟病的C语言函数指针体系了,在C语言中,如果我们想将一个函数作为参数传给其他函数,那么我们就要在形参列表中准确写出函数指针的类型。但是函数指针经常会非常复杂,用起来很难受。

但是对象就不一样了,如果我们将仿函数的对象作为参数传递,此时的类型就非常好写了。

其次是内联函数的问题:

仿函数是定义在类中的,类中的成员函数默认为内联函数,此时短小的函数就会被展开。而如果是一般的函数,用inline修饰后,由于要在指定位置展开,此时就没有地址,没有函数指针了,所以内联函数无法作为参数传递。如果inline修饰的函数被作为函数指针传递,此时这个函数的inline就会失效,从而无法展开。而仿函数很好解决了这个问题

所以仿函数有两大优点:

  1. 避免了复杂的函数指针体系,我们可以很方便的将仿函数作为参数传递
  2. 仿函数可以作为参数传递的同时,还支持内联函数

对于我们的优先级队列,我们就可以使用仿函数作为一个模板参数。
用户使用优先级队列时,可以自己传入一个仿函数,从而制定自己的规则,按照自己设想的方法来排列数据。

比大小的仿函数:

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 Compare = less<T>>
class priority_queue
{
public:
	void adjust_up(int x)
	{
		int child = x;
		int parent = (child - 1) / 2;

		while (child > 0)
		{
			if (Compare()(_con[parent], _con[child]))
			{
				swap(_con[child], _con[parent]);
				child = parent;
				parent = (child - 1) / 2;
			}
			else
			{
				break;
			}
		}
	}

	void adjust_down(int x)
	{
		int parent = x;
		int child = 2 * parent + 1;

		while (child < _con.size())
		{
			if (child + 1 < _con.size() && Compare()(_con[child], _con[child + 1]))
			{
				child++;
			}

			if (Compare()(_con[parent], _con[child]))
			{
				swap(_con[child], _con[parent]);
				parent = child;
				child = 2 * parent + 1;
			}
			else
			{
				break;
			}
		}
	}

	void push(const T& x)
	{
		_con.push_back(x);
		adjust_up(_con.size() - 1);
	}

	void pop()
	{
		swap(_con[0], _con[_con.size() - 1]);
		_con.pop_back();
		adjust_down(0);
	}

	const T& top()
	{
		return _con[0];
	}

	size_t size()
	{
		return _con.size();
	}

	bool empty()
	{
		return _con.empty();
	}

private:
	Container _con;
};

在模板参数中,我们传入了第二个参数,用于接受一个仿函数Compare ,这个仿函数用于决定后续比较时的优先级规则。

后续我们在比较parentchild大小的地方调用了仿函数:Compare()(_con[parent], _con[child])
这里并没有多写一对括号,Compare()是一个匿名对象,而后面的(_con[parent], _con[child])则是在调用operator()

当然,如果你认为这样可读性太差了,也可以在类中多定义一个compare类的对象cmp,后续直接cmp(_con[parent], _con[child])这样调用这个仿函数。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/387035.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

NSSCTF Round#18 RE GenshinWishSimulator WP

恶搞原神抽卡模拟器 看到软件的界面&#xff0c;大致有三种思路&#xff1a; 修改石头数量一直抽&#xff0c;如果概率正常肯定能抽到&#xff08;但是估计设置的概率是0&#xff09;在源码里找flag的数据把抽卡概率改成100%直接抽出来 Unity逆向&#xff0c;根据经验应该dnsp…

助眠神器小程序源码|白噪音|小睡眠|微信小程序前后端开源

安装要求和说明后端程序运行环境&#xff1a;NginxPHP7.4MySQL5.6 PHP程序扩展安装&#xff1a;sg11 网站运行目录设置为&#xff1a;public 伪静态规则选择&#xff1a;thinkphp 数据库修改文件路径&#xff1a;/config/database.php需要配置后端的小程序配置文件&#xff0c;…

力扣hot1--哈希

推荐一个博客&#xff1a; 一文看懂哈希表并学会使用C STL 中的哈希表_哈希表end函数-CSDN博客 哈希做法&#xff1a; 我们将nums[i]记为key&#xff0c;将i记为value。 判断target-nums[i]是否在哈希表中&#xff0c;如果在说明这两个值之和为target&#xff0c;那么返回这两…

【Java】零基础蓝桥杯算法学习——线性动态规划(一维dp)

线性dp——一维动态规划 1、考虑最后一步可以由哪些状态得到&#xff0c;推出转移方程 2、考虑当前状态与哪些参数有关系&#xff0c;定义几维数组来表示当前状态 3、计算时间复杂度&#xff0c;判断是否需要进行优化。 一维动态规划例题&#xff1a;最大上升子序列问题 Java参…

Linux第48步_编译正点原子的出厂Linux内核源码

编译正点原子的出厂 Linux 内核源码&#xff0c;为后面移植linux做准备。研究对象如下&#xff1a; 1)、linux内核镜像文件“uImage” 路径为“arch/arm/boot”&#xff1b; 2)、设备树文件“stm32mp157d-atk.dtb” 路径为“arch/arm/boot/dts” 3)、默认配置文件“stm32m…

第二部分阶段总结

第二部分阶段总结 1.知识补充1.1 nolocal关键字1.2 yield from1.3 深浅拷贝 2.阶段总结3.考试题 1.知识补充 1.1 nolocal关键字 在之前的课程中&#xff0c;我们学过global关键字。 name rootdef outer():name "武沛齐"def inner():global namename 123inner()…

LeetCode、739. 每日温度【中等,单调栈】

文章目录 前言LeetCode、739. 每日温度【中等&#xff0c;单调栈】题目链接及分类思路单调栈 资料获取 前言 博主介绍&#xff1a;✌目前全网粉丝2W&#xff0c;csdn博客专家、Java领域优质创作者&#xff0c;博客之星、阿里云平台优质作者、专注于Java后端技术领域。 涵盖技…

【每日一题】尾随零

尾随零 目录 思路&#xff1a;代码实现&#xff1a; 思路&#xff1a; 最开始看到这题就只想到规规矩矩的做题&#xff0c;先算阶乘在算0&#xff0c;后来提交时总是提示溢出&#xff0c;不死心&#xff0c;改来改去最后没招了。 后来看题解才知道要看5的个数&#xff01; …

Java 基于 SpringBoot+Vue 的智慧外贸平台的研究与实现,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

CVE-2023-41892 漏洞复现

CVE-2023-41892 开题&#xff0c;是一个RCE Thanks for installing Craft CMS! You’re looking at the index.twig template file located in your templates/ folder. Once you’re ready to start building out your site’s front end, you can replace this with someth…

【C++入门语法】1.变量的世界

​ 欢迎来到C的世界&#xff01;在这篇文章中&#xff0c;我们将一起探索C编程中的基本概念——变量。变量是程序设计中非常重要的一部分&#xff0c;它们是存储数据的容器&#xff0c;让我们的程序能够记住和操作这些信息。 什么是变量&#xff1f; 变量是一个标识符&#x…

Python基于大数据的电影预测分析系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

steam搬砖项目,“一个月赚8K+”真的假的?

在游戏中&#xff0c;搬砖党是永远都不能忽视的存在&#xff0c;随着游戏产业的不断发展&#xff0c;普通人也可以在steam搬砖项目中找到自己的生财之道。由于是低技术的重复工作&#xff0c;和现实的搬砖类似&#xff0c;所以才叫steam搬砖项目。 steam搬砖项目其实就和pdd无…

【RL】Bellman Optimality Equation(贝尔曼最优等式)

Lecture3: Optimal Policy and Bellman Optimality Equation Definition of optimal policy state value可以被用来去评估policy的好坏&#xff0c;如果&#xff1a; v π 1 ( s ) ≥ v π 2 ( s ) for all s ∈ S v_{\pi_1}(s) \ge v_{\pi_2}(s) \;\;\;\;\; \text{for all…

TypeScript 入门

课程地址 ts 开发环境搭建 npm i -g typescript查看安装位置&#xff1a; $ npm root -g C:\Users\Daniel\AppData\Roaming\npm\node_modules创建 hello.ts&#xff1a; console.log("hello, ts");编译 ts 文件&#xff0c;得到 js 文件&#xff1a; $ tsc foo.…

华为机考入门python3--(14)牛客14-字符串排序

分类&#xff1a;列表、排序 知识点&#xff1a; 字典序排序 sorted(my_list) 题目来自【牛客】 def sort_strings_by_lex_order(strings): # 使用内置的sorted函数进行排序&#xff0c;默认是按照字典序排序 sorted_strings sorted(strings) # 返回排序后的字符串列…

H5 渐变3D旋转个人主页引导页源码

H5 渐变3D旋转个人主页引导页源码 源码介绍&#xff1a;一款渐变3D旋转个人主页引导页源码&#xff0c;可以做个人主页/旗下网站引导 下载地址&#xff1a; https://www.changyouzuhao.cn/10392.html

linux信号机制[二]

阻塞信号 信号相关概念 实际执行信号的处理动作称为信号递达(Delivery)信号从产生到递达之间的状态,称为信号未决(Pending)。[收到信号但是没有处理]进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.注…

“从根到叶:深入理解堆数据结构“

​​​​​​​ 一.堆的概念及实现 1.1堆的概念 在数据结构中&#xff0c;堆是一种特殊的树形数据结构。堆可以分为最大堆和最小堆两种类型。 最大堆&#xff1a;对于堆中的任意节点&#xff0c;其父节点的值都不小于它的值。换句话说&#xff0c;最大堆中的根节点是堆中的最…

猫头虎分享已解决Bug || Invariant Violation in React: Element Type is Invalid ‍

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …