C++STL【priority_queue 优先级队列】

本文介绍了C++中的priority_queue数据结构,它是基于vector和堆算法的实现,重点讨论了仿函数的应用、堆的上下调整策略以及时间复杂度分析。作者还模拟了STL库中的priority_queue并提供了相关操作的源代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

priority_queue 优先级队列

介绍

priority_queue,优先级队列,它的底层是个vector,在vector的基础上封装堆的算法,于是它摇身一变,成了一个存储在一块连续空间中的堆。
《什么是堆?》 堆是一棵完全二叉树,特点是:所有子节点小于或等于父节点 / 所有子节点大于或等于父节点。
而所谓优先级,就是大堆和小堆的区分。
如此看来,优先级队列似乎一点也不神秘。

但它还是有一些值得我们学习的地方的,不只是源码的实现细节。

思索

1)仿函数
顾名思义,模仿函数。那么谁来模仿呢,当然是C++最常用的东西——类。
其实就是写一个类,在类中重载运算符operator(),使得这个类对象可以像函数一样调用。
表面上是调用了一个函数,其实是调用重载运算符operator(),然后运行其下的代码。

举个小例子:

template<class T>
struct com_less // 这是一个仿函数类,用来比较大小(左参数小于右参数)
{
	bool operator()(const T& left, const T& right)
	{
		return left < right;
	}
};

void Test()
{
	com_less<int> com;

	cout << com(1, 2) << endl;
}
// 输出结果为1

很简单吧。
在C语言中,函数指针也可也达到这样的效果,但是不同的是,C++的仿函数更加的“泛型编程”,满足STL对抽象性的要求,可以很好的和STL的其他组件进行搭配。比如和配接器搭配,应用于各个容器之中。

在这里插入图片描述

关于仿函数的更多知识,博主尚未掌握,诸如仿函数的种类等等,日后有机会再详谈。

2)刚才说了,priority_queue底层是vector + 堆算法实现的,在查看STL中priority_queue的源码时候,我发现库中将priority_queue关于堆的操作封装了(make_heap/push_heap/pop_heap)。

在这里插入图片描述

最坑的是,当时的我并不知道堆算法在一个名为"stl_heap.h"的头文件中,于是找了半天。同样出于好奇,我想知道库里用的是向下调整还是向上调整,最终,我探到了它的底牌:
库中使用向下调整建堆。

在这里插入图片描述

为什么不用向上调整建堆?
这与两种建堆方法的时间复杂度有关。还是这篇文章
我们以满二叉树为例,通过计算,求出向上调整建堆和向下调整建堆的时间复杂度:

在这里插入图片描述

如果你信得过我的话(当然,我是挺自信的),一个是O(N*logN),一个是O(N),自然,向下调整建堆的效率更高,猜想得证。

模拟

参考STL库源码,我对priority_queue模拟了一番:

#pragma once
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;

namespace Myspace
{
	// 实现仿函数
	template<class T>
	struct less // less是建大堆
	{
		bool operator()(const T& val1, const T& val2)
		{
			return val1 < val2;
		}
	};

	template<class T>
	struct greater // greater是建小堆
	{
		bool operator()(const T& val1, const T& val2)
		{
			return val1 > val2;
		}
	};

	// 优先级队列类
	template<class T, class Container = vector<T>, class Compare = less<T>>
	class priority_queue
	{
		// 向下调整
		void AdjustDown(int parent)
		{
			Compare com; // 仿函数

			int child = parent * 2 + 1;
			while (child < _con.size())
			{
				if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))	// 先假设建大堆,则需要找到左右孩子中大的那个
				{
					child += 1;
				}
				if (child < _con.size() && com( _con[parent], _con[child]))
				{
					swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
					break;
			}
		}

		// 向上调整
		void AdjustUp(int child)
		{
			Compare com; // 仿函数

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

		void make_heap()
		{
			// 建堆 - 使用向下调整
			for (int i = (_con.size() - 2) / 2; i >= 0; i--)
			{
				AdjustDown(i);
			}
		}

	public:
		priority_queue()
		{ }

		template <class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				_con.push_back(*first);
				++first;
			}
			make_heap();
		}

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

		void pop()
		{
			assert(!empty());
			// 删除的是头元素
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			AdjustDown(0);
		}

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

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

		const T& top() const
		{
			return _con.front();
		}

	private:
		Container _con;
	};
}

需要注意的是:

  1. 每次push时,都是在vector的最后插入一个元素,也就是二叉树的最下、最右的位置,因此只需一次向上调整就可以把新元素正确地插入堆中。
  2. pop删除的是根节点,也就是vector第一个元素,此时将头、尾两个元素交换,将新的尾删除,再将新的头向下调整一次即可。
  3. 相对于插入、删除一个元素,建堆操作是在vector已有一些元素之后,对整个vector进行排序建堆。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值