【C++杂货铺】优先级队列的使用指南与模拟实现

文章目录

一、priority_queue的介绍

  • 优先级队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。

  • 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先级队列中位于顶部的元素)。

  • 优先级队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue 提供一组特定的成员函数来访问其元素。元素从特定容器的"尾部"弹出,其称为优先级队列的顶部。

  • 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问、迭代器访问,并支持以下操作:

    • empty():检测容器是否为空

    • size():返回容器中有效元素的个数

    • front():返回容器中第一个元素的引用

    • push_back():在容器尾部插入元素

    • pop_back():删除容器尾部元素

  • 标准容器类 vector 和 deque 满足这些需求。默认情况下,如果没有为特定的 priority_queue 类实例化指定容器类,则使用 vector。

  • 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数 make_heap、push_heap 和 pop_heap 来自动完成次操作。

二、priority_queue的使用

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

函数声明 接口说明
priority_queue() 构造一个空的优先级队列
empty() 检测优先级队列是否为空,是返回 true,否则返回 false
top() 返回优先级队列中最大(最小)元素,即堆顶元素
push(x) 在优先级队列中插入元素 x
pop() 删除优先级队列中最大(最下)元素,即堆顶元素

小Tips:默认情况下,priority_queue 是大堆(默认第三个模板参数是 less);如果在 priority_queue 中放自定义类型的数据,用户需要在自定义类型中提供 > 或者 < 的重载。

2.1 数组中的第k个最大元素

cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        //建一个大堆,向下调整建堆的时间复杂度是O(N)
        priority_queue<int> pq(nums.begin(), nums.end());

        //pop k-1次,时间复杂度是O(K*logN)
        while(--k)
        {
            pq.pop();
        }

        return pq.top();
    }
};

上面这种做法当 K 的值接近 N 的时候,它的时间复杂度就是 O(N*logN),是不满足题目要求的,下面再用另外一种方法来解决。

cpp 复制代码
lass Solution {
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        //用前k个数建一个小堆
        priority_queue<int, vector<int>, greater<int>> pq(nums.begin(), nums.begin() + k);

        //遍历剩下的 N-k 个,比堆顶大就换进去
        //时间复杂度是 (N-K)logN
        for(size_t i = k; i < nums.size(); i++)
        {
            if(nums[i] > pq.top())
            {
                pq.pop();
                pq.push(nums[i]);
            }
        }
        return pq.top();
    }
};

上面这种解法是先用数组中的前 K 个元素建一个小堆,然后从数组的第 K+1 个元素开始往后遍历,遇到比堆顶元素大的就将堆顶的元素 pop 出来,将当前元素 push 进去。建堆的时间复杂度是 O(K),更新小堆的时间复杂度是 O((N-K)logK),这种做法当 K 很小的时候时间复杂度可以近似看做 O(NlogK),当 K 很大的时候,时间复杂度可以近似看做 O(logK)。

三、priority_queue模拟实现

3.1 仿函数

仿函数本质上一个类,之所以把它叫仿函数是因为这个类的对象可以像函数一样去使用。举例如下:

cpp 复制代码
//一个仿函数
template<class T>
class Less
{
public:
	bool operator()(const T& x, const T& y)
	{
		return x < y;
	}
};


int main()
{
	Less<int> lessfunc;//定义一个对象
	cout << lessfunc(1, 2) << endl;//该类型对象可以像函数一样去使用
	//cout << lessfunc.operator()(1, 2) << endl;//和上面等价
	return 0;
}

小Tips:仿函数一般都是类模板,这样就可以支持不同类型的大小比较,前提是该种类型重载实现了比较运算符。仿函数的诞生是为了代替函数指针,函数指针的可读性比较差。

3.2 成员变量

cpp 复制代码
template<class T, class Container=std::vector<T>, class Compare = Less<T>>
	class priority_queue
	{
	public:
		//成员
	private:
		Container _con;
	};

小Tips:需要注意这里有三个模板参数,第一个模板参数表示要在优先级队列中存储的数据类型;优先级队列本质上也是一个容器适配器,所以第二个模板参数表示优先级队列要使用的底层容器;第三个模板参数是一个仿函数,用来进行大小比较的,因为优先级队列中会涉及到建大堆还是建小堆,中间会涉及到比较,如果没有这个仿函数,那么大小比较就只能写死了,这样不太好。

3.3 成员函数

3.3.1 构造函数

cpp 复制代码
priority_queue()
{}

template<class InputeIterator>
priority_queue(InputeIterator first, InputeIterator last)
{
	//先将数据插入
	while (first != last)
	{
		_con.push_back(*first);
		++first;
	}

	//建堆,从最后一个非叶子节点开始向下调整
	for (int i = (_con.size() - 1) / 2; i >= 0; i--)
	{
		AdjustDown(i);
	}
}

小Tips:迭代器区间构造函数是一个函数模板,只要是能支持 ++ 操作的迭代器区间都可以,即单向迭代器、双向迭代器、随机迭代都可以。其次将数据插入容器后还需要建堆,这里采用向下调整建堆,它的时间复杂度是 O(N)。

3.3.2 AdjustDown

cpp 复制代码
void AdjustDown(int parent)
{
	Compare com;
	while (parent * 2 + 1 < (int)_con.size())
	{
		//找到最大的孩子
		int maxchild = parent * 2 + 1;
		if (maxchild + 1 < (int)_con.size() && com(_con[maxchild], _con[maxchild + 1]))
		{
			maxchild++;
		}

		//和父节点比较
		if (com(_con[parent], _con[maxchild]))
		{
			std::swap(_con[maxchild], _con[parent]);
			parent = maxchild;
		}
		else
		{
			break;
		}
	}
}

小Tips:在仿函数的加持下,向下调整代码中的大小比较不再固定,建大堆和小堆这一份代码就够了,最终是大堆还是小堆是由仿函数来控制的。

3.3.3 push

cpp 复制代码
void push(const T& val)
{
	_con.push_back(val);
	AdjustUp(_con.size() - 1);
}

小Tips:在优先级队列的尾部追加数据,会涉及到向上调整,向上调整的代码如下所示。

3.3.4 AdjustUp

cpp 复制代码
void AdjustUp(int child)
{
	Compare com;
	int parent = (child - 1) / 2;
	while (child > 0)//父亲不会小于0,因此这里的判断条件要用孩子大于0,孩子等于0说明已经调整到根节点,就无需继续调整了
	{
		if (com(_con[parent], _con[child]))
		{
			std::swap(_con[child], _con[parent]);
			child = parent;
			parent = (child - 1) / 2;//这里parent不会小于0
		}
		else
		{
			break;
		}
	}
}

3.3.5 pop

cpp 复制代码
void pop()
{
	std::swap(_con[0], _con[_con.size() - 1]);
	_con.pop_back();
	AdjustDown(0);
}

小Tips:优先级队列中出数据,出的是堆顶的数据,堆顶的数据也就是容器中的第一个数据,如果底层容器是 vector 那么堆顶的数据就是下标为 0 的数据,出堆顶的数据不能直接使用头删,这样会导致后面数据的父子关系全乱了。正确的做法是,将堆顶的数据和容器中的最后一个数据交换,然后执行 pop_back 去删除,最后还需要从根节点开始执行一次向下调整,让交换到堆顶的数据去到它应该去的地方。

3.3.6 empty

csharp 复制代码
bool empty()
{
	return _con.size() == 0;
}

3.3.7 size

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

四、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

相关推荐
无尽的大道8 分钟前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
爱吃生蚝的于勒11 分钟前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
羊小猪~~15 分钟前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
binishuaio21 分钟前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE22 分钟前
【Java SE】StringBuffer
java·开发语言
就是有点傻27 分钟前
WPF中的依赖属性
开发语言·wpf
洋24035 分钟前
C语言常用标准库函数
c语言·开发语言
进击的六角龙37 分钟前
Python中处理Excel的基本概念(如工作簿、工作表等)
开发语言·python·excel
wrx繁星点点38 分钟前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
NoneCoder1 小时前
Java企业级开发系列(1)
java·开发语言·spring·团队开发·开发