C++初阶-priority_queue(优先级队列)(堆)

目录

​​​​​​1.std::priority_queue简介

2.std::priority_queue

3.std::priority_queue的成员函数

3.1std::priority_queue的构造函数

3.2std::priority_queue::empty

3.3std::priority_queue::size

3.4std::priority_queue::top

3.5std::priority_queue::emplace和std::priority_queue::push

3.6std::priority_queue::pop和std::priority_queue::swap

4.*手动实现std::priority_queue主要的函数(不是很重要)

4.1基本结构

4.2仿函数的实现

4.3std::priority_queue::push函数的模拟实现

4.4std::priority_queue::pop的模拟实现

4.5其余重要的成员函数的实现

4.6最终代码

5.在OJ题中priority_queue的使用

6.总结


1.std::priority_queue简介

在C++官网中,链接为:

Reference - C++ Referencehttps://legacy.cplusplus.com/reference搜索queue,会出现:   

在左下角可以看到priority_queue这个容器,它和queue放在同一个头文件,只要在使用它时

#include<queue> 即可使用这个容器。我们知道queue是队列,在数据结构中,队列是有先进先出的性质的,而在priority_queue(优先级队列)中也有同样的性质,但是它是优先级高的先出,默认情况下是大的优先级高,不过我们可以通过仿函数让小的优先级高

通过这些性质我们发现这个容器很像我们之前学过的数据结构-堆,实际上它的底层就是堆。

2.std::priority_queue

在C++官网中我们进入这个容器,会出现这个界面:

如果我们把这个容器的简介翻译一下就有了以下结果:

如果不了解堆的话,可能这些概念是理解不了的,如果你不了解堆,就去看我之前写的博客:

数据结构初阶-堆的代码实现-CSDN博客文章浏览阅读1.1k次,点赞33次,收藏29次。堆这个数据结构实现起来比较难,日后需要不断去强化这方面的知识。喜欢的可以一键三连哦,下节讲解:二叉树的链式实现。 https://blog.csdn.net/2401_86446710/article/details/146079085?spm=1011.2415.3001.10575&sharefrom=mp_manage_link通过观察这个函数的声明:

template <class T, class Container = vector<T>,
  class Compare = less<typename Container::value_type> > class priority_queue;

我们可以发现它的底层是用vector实现的,就是我们熟知的顺序表,最后一个模板参数是仿函数,这个less实际上是一个类,如果你想建大堆就可以不传递这个Compare,如果你想建小堆就可以传递一个greater<int>这种:

#include<iostream>
#include<vector>
#include<queue>
using namespace std;
int main()
{
	//建大堆
	priority_queue<int> pq;
	//建小堆
	priority_queue<int, vector<int>, greater<int>> qp;
}

仿函数的知识我会在下一讲:C++初阶-仿函数进行讲解。

3.std::priority_queue的成员函数

在priority_queue界面中可以看到:

3.1std::priority_queue的构造函数

构造函数即(consturctor),我们可以点进去看到:

C++11的构造函数太多了,我们现阶段用得多的还是C++98的两个构造函数,所以我就讲解这两个的用法即可,C++11版本的那些方法用的不是很多,感兴趣的可以去了解一下:

第一个构造函数是无参的构造,也就是说没有数据的堆进行初始化;第二个是用一段迭代器区间进行初始化,这个迭代器区间可以是任意支持迭代器的容器,也可以是数组都可以。如:

//构造函数的使用
int main()
{
	//(1)
	priority_queue<int> q;
	//(2)
	vector<int> v({ 4,6,2,7,8,1,0,9 ,5});
	priority_queue<int> p(v.begin(), v.end());
	while (!p.empty())
	{
		cout << p.top() << ' ';
		p.pop();
	}
	cout << endl;
	return 0;
}

那么运行的结果就是:

我们可以观察到这个运行结果刚好是有序的,这是堆的一个性质,总是把最大的元素放到堆顶,当堆顶没有数据的时候也会自己再把最大的数据放到堆顶。

3.2std::priority_queue::empty

这个函数我们在vector、string等容器中都见过,就是判空,如果堆是空就返回true,如果堆不是空就返回false:

这个函数比较简单,就不做过多介绍了。

3.3std::priority_queue::size

这个函数是用来计算堆的大小的即堆中的数据个数:

它的返回值是size_type,就是我们熟知的size_t(无符号整型)类型,我们使用时可以这样:

//size的使用
int main()
{
	vector<int> v({ 4,6,2,7,8,1,0,9 ,5 });
	priority_queue<int> p(v.begin(), v.end());
	cout << p.size() << endl;
	return 0;
}

则结果为:

3.4std::priority_queue::top

top就是我们熟知的返回堆顶元素,一般是与pop结合使用得到一个有序的序列。在默认情况下,堆顶元素是这个堆最大的元素;如果是用greater作为仿函数,则是这个堆最小的元素。

3.5std::priority_queue::emplace和std::priority_queue::push

这两个函数都是插入元素,如果插入的元素比堆顶元素优先级高或者堆里面没有元素的时候就会放到堆顶,但是二者都有一定的区别,push支持传递临时对象作为参数,也支持传递已经构造的对象作为参数,如:

#include<queue>
#include<string>
using namespace std;
//push的使用
int main()
{
	priority_queue<string> p;
	//(1)
	string a("abcdefg");
	p.push(a);
	//(2)
	p.push({ "hijklmn" });
	while (!p.empty())
	{
		cout << p.top() << ' ';
		p.pop();
	}
	cout << endl;
	return 0;
}

则结果为:

如果是用emplace的话,那这个插入方式就简便多了:

//emplace的使用
void test()
{
	priority_queue<pair<string, int>> b;
	b.emplace("China", 1);
	b.emplace("HuNan", 2);
	//不可以
	//b.push("China",1);
	while (!b.empty()) 
	{
		cout << b.top().first << ": " << b.top().second << endl;
		b.pop();
	}
	cout << endl;
}
int main()
{
	priority_queue<string> p;
	//(1)
	string a("abcdefg");
	p.emplace(a);
	//不支持
	//p.emplace({ "hijklmn" });
	//(2)
	p.emplace("hijklmn");
	//(3)
	//插入四个x
	// 构造 std::string(4, 'x') → "xxxx"
	p.emplace(4, 'x');
	while (!p.empty())
	{
		cout << p.top() << ' ';
		p.pop();
	}
	cout << endl;
	test();
	return 0;
}

那么运行结果为:

其中void test这个里面的pair是一个键值对,也是一个类,不过这个类能存储两个值:

简单来说这个pair能实现一个值对应另外一个值,我会在C++进阶-map和set里面讲解一下,主要是为了演示一下emplace的用法,如果我们把2和HuNan交换,那么会运行报错的,因为emplace是自动构建对象,这样的方式不能构造,对应的我们可以这样:

priority_queue<string> p;
string b("qwertyuiop");
p.emplace(b.begin() + 1, b.end() - 1);
p.emplace("8901234567", 4);
p.emplace(b, 0, 4);
string a("abcdefg");
p.emplace(a);
p.emplace("hijklmn");
p.emplace(4, 'x');

那么结果就是:

我能这样做是因为string类里面有这种构造函数:

我们在使用emplace的时候可以按照对应的构造函数的参数顺序进行传递相当于我们如果传递第一个参数为size_t类型,那么就对应第6个构造函数,但是第二个参数必须要传递为char类型,因为要构造一个对象然后插入到堆里面,所以我们必须保证参数的顺序和类型在该对象的有这个构造函数!

我的建议是用emplace的时候就是没有创建对应的对象的时候用,而在堆中插入已经创建对应的对象的时候直接插入该对象即可,这个时候用push最好。

3.6std::priority_queue::pop和std::priority_queue::swap

pop这个是删除堆顶元素并且把堆顶用优先级最高的元素补齐(堆还有元素的情况下),swap则是交换两个堆,但是这两个堆的各个模板参数都是要相同的,否则可能会交换失败。

4.*手动实现std::priority_queue主要的函数(不是很重要)

手动实现堆其实不是很重要,但是我们要理解它是如何实现的方便我们后面就业的时候使用!

4.1基本结构

#include<iostream>
#include<vector>
#include<queue>
using namespace std;
namespace td
{
	template<class T,class Container = vector<T>>
	class priority_queue
	{
	private:
		Container _con;
	};
}

4.2仿函数的实现

我们在std::priority_queue里面还有一个模板参数是Compare,这个可以手动实现,也可以直接调用算法库里面的Less和Greater仿函数,它们定义在 <functional> 头文件中,这里我就直接实现了,下讲再进行代码讲解。

//小的优先级高
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;
	}
};

那么原类就要改成这样:

template<class T,class Container = vector<T>,class Compare = Less<T>>
class priority_queue
{
private:
	Container _con;
};

4.3std::priority_queue::push函数的模拟实现

我之前写的博客里面讲过如何实现,博客链接:

数据结构初阶-堆的代码实现-CSDN博客文章浏览阅读1.1k次,点赞33次,收藏29次。堆这个数据结构实现起来比较难,日后需要不断去强化这方面的知识。喜欢的可以一键三连哦,下节讲解:二叉树的链式实现。 https://blog.csdn.net/2401_86446710/article/details/146079085?spm=1011.2415.3001.10575&sharefrom=mp_manage_link这个堆的插入我们在C++阶段是直接用vector实现的,所以直接先尾插后再进行向上调整算法,所以:

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

向上调整算法是通过比较child和parent的优先级然后优先级高的就放到上面,优先级低的放到下面,如果产生了交换就继续比较,并且让child=parent;parent=(child-1)/2;(注:这个child和parent是下标,不是所存储的数据类型)开始parent也为(child-1)/2,如果没产生交换就直接结束向上调整,但是如何比较二者优先级呢

我们可以借助仿函数进行比较,比如:Compare com;com(_con[child],_con[parent]);(前提是_con这个容器支持[]的下标访问,否则这个是不行的,但是我这里只用默认的vector,需要改进的记得改一下这个比较),这样com(_con[child],_con[parent]);的返回值就是true和false如果true就代表child这个位置的优先级高就交换即可。那么最终代码如下:

void adjust_up(size_t child)
{
	size_t parent = (child - 1) / 2;
	Compare com;
	while (child > 0)
	{
		if (com(_con[parent], _con[child]))
		{
			swap(_con[child], _con[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void push(const T& x)
{
	_con.push_back(x);
	adjust_up(_con.size() - 1);
}

4.4std::priority_queue::pop的模拟实现

这个函数实现需要先把堆顶元素与最后一个元素交换(堆顶就是第一个元素),基本代码如下:

void adjust_down(size_t parent)
{

}
void pop()
{
	swap(_con[0], _con[_con.size() - 1]);
	//删除最后一个元素相当于原堆顶元素被删除
	_con.pop_back();
	//向下调整
	adjust_down(0);
}

然后再进行向下调整,直到找到优先级最高的一个元素放到堆顶即可,但是有两个孩子,一个是2*parent+1(左孩子),另外一个是2*prarent+2(右孩子),但是左孩子存在并不代表右孩子存在,所以我们要先假设只有一个孩子,而这个孩子一定是左孩子(堆的规则),然后先进入循环,判断是否左孩子超出了堆的大小,再判断是否存在右孩子并且右孩子比左孩子优先级高,再把优先级高的孩子与parent进行比较,比较结束后再判断是否要进行交换,如果发生了交换就继续向下调整,否则结束,这个判断也可以直接通过仿函数来进行,所以最终代码如下:

void adjust_down(size_t parent)
{
	Compare com;
	//先假设只有左孩子
	size_t child = 2 * parent + 1;
	while (child < _con.size())
	{
		//判断是否有右孩子并且是否要进行交换
		if (child + 1 < _con.size() && com(_con[child], _con[parent]))
		{
			++child;
		}
		//判断是否交换
		//返回的是flase代表parent的优先级高,顺序不能改变!!!
		if (com(_con[parent], _con[child]))
		{
			swap(_con[child], _con[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void pop()
{
	swap(_con[0], _con[_con.size() - 1]);
	//删除最后一个元素相当于原堆顶元素被删除
	_con.pop_back();
	//向下调整
	adjust_down(0);
}

4.5其余重要的成员函数的实现

其他成员函数如:top、size、empty实现都比较简单,所以这里就直接给了:

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

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

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

4.6最终代码

这个代码是仅供参考,因为写法有多种多样,该代码已经经过测试了,没有问题的:

#include<iostream>
#include<vector>
#include<queue>
using namespace std;
namespace td
{
	//小的优先级高
	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;
		}
	};
	template<class T,class Container = vector<T>,class Compare = Less<T>>
	class priority_queue
	{
	public:
		void adjust_up(size_t child)
		{
			size_t parent = (child - 1) / 2;
			Compare com;
			while (child > 0)
			{
				if (com(_con[parent], _con[child]))
				{
					swap(_con[child], _con[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
		void push(const T& x)
		{
			_con.push_back(x);
			adjust_up(_con.size() - 1);
		}
		void adjust_down(size_t parent)
		{
			Compare com;
			//先假设只有左孩子
			size_t child = 2 * parent + 1;
			while (child < _con.size())
			{
				//判断是否有右孩子并且是否要进行交换
				if (child + 1 < _con.size() && com(_con[child], _con[parent]))
				{
					++child;
				}
				//判断是否交换
				//返回的是flase代表parent的优先级高,顺序不能改变!!!
				if (com(_con[parent], _con[child]))
				{
					swap(_con[child], _con[parent]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}
		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;
	};
}

5.在OJ题中priority_queue的使用

这个题目比较简单,题目链接:

215. 数组中的第K个最大元素 - 力扣(LeetCode)https://leetcode.cn/problems/kth-largest-element-in-an-array/description/题目:

这个题目如果用priority_queue就变得非常简单,我们直接把所有元素push进去,然后再pop  k-1个元素,最后top即可,代码如下:

class Solution 
{
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        priority_queue<int> p;
        for(const auto& e:nums)
        {
            p.push(e);
        }
        while(--k)
        {
            p.pop();
        }
        return p.top();
    }
};

当然我们也可以直接用一段迭代器区间进行初始化,就不用一个一个赋值了:

class Solution 
{
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        priority_queue<int> p(nums.begin(),nums.end());
        while(--k)
        {
            p.pop();
        }
        return p.top();
    }
};

不过这种方式消耗内存太多了,这不是最好的解法:

我们解答这题的时候可以直接创建一个大小为K的静态堆,然后把最小的放在堆顶,即建小堆,之后如果大于堆顶才插入,否则直接跳过,这样更好,所以代码如下:

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        priority_queue<int,vector<int>,greater<int>> p(nums.begin(),nums.begin()+k);
        int c=p.top();
        for(int i=k;i<nums.size();i++)
        {
            if(nums[i]>c)
            {
              p.pop();
              p.push(nums[i]);
              c=p.top();
            }
        }
        return p.top();
    }
};

这个代码已经可以了:

6.总结

priority_queue是一个比较重要的容器,它可以用来进行排序,不过现阶段用算法库里面的sort函数已经足够了,堆的应用还是比较多的,我讲的只是使用,题目中可能会用到堆,希望你们能掌握它的用法!好了,这一讲就到这里,下一讲将讲解:仿函数。喜欢的可以一键三连哦,下讲再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刚入门的大一新生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值