【C++】优先级队列与仿函数

                                                

🔥个人主页北辰水墨

🔥专栏C++学习仓

Alt

本节内容我们来讲解优先级队列和仿函数。文中会附上优先级队列模拟实现的源码。

注意:本节我会把最大优先级队列和大堆名词混着用,他们两个本质是一样的。

一、priority_queue的介绍和使用: 

1.  优先队列是一种容器适配器,它的第一个元素总是它所包含的元素中最大的(大堆)。
2.  此上下文类似于,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。

3.  容器应该可以通过随机访问迭代器访问,并支持以下操作:

  • empty():检测容器是否为空
  • size():返回容器中有效元素个数
  • front():返回容器中第一个元素的引用
  • push_back():在容器尾部插入元素
  • pop_back():删除容器尾部元素

4.priority_queue优先级队列应该支持以下操作:(点击即可跳转到cplusplus网站查看相关用法)

  • empty()  检测优先级队列是否为空
  • size()      返回优先级队列中有效元素个数
  • top()       取堆顶的元素
  • push()    插入元素,并且向下调整算法,保持堆属性
  • pop()      删除堆顶元素,交换堆顶和最后一个数据,然后向上调整算法,再size--;
  • swap()    交换两个堆中的所有元素

5.  标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector
6.  需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。

函数使用

T:类型模板参数

Container:用来内部存储优先级队列中元素的容器类型。

Compare:这是用来比较元素优先级的比较函数对象。默认是 std::less,该函数使得最大的元素被认为是最高优先级(形成最大堆)。如果想要最小的元素为最高优先级(形成最小堆),可以通过提供 std::greater 函数对象作为这个模板参数来改变这个行为

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

🔥构造函数 

 创建一个优先级队列:

创建最大优先级队列(大堆)std::priority_queue<int> q1;

创建最小优先级队列(小堆)std::priority_queue<int,std::vector<int>,std::greater<int>> q2;

🔥empty( )

检测优先级队列是否为空,是返回true,否则返回false

🔥top( )

返回优先级队列中最大(最小元素),即堆顶元素

🔥push( )

在优先级队列中插入元素x

🔥pop( )

删除优先级队列中最大(最小)元素,即堆顶元素

 默认情况下是大堆:

// priority_queue::push/pop
#include <iostream>       // std::cout
#include <queue>          // std::priority_queue

int main ()
{
  std::priority_queue<int> mypq;

  mypq.push(30);
  mypq.push(100);
  mypq.push(25);
  mypq.push(40);

  std::cout << "Popping out elements...";
  while (!mypq.empty())
  {
     std::cout << ' ' << mypq.top();
     mypq.pop();
  }
  std::cout << '\n';

  return 0;
}

它会自动调成大堆,测试结果如下:

        Popping out elements... 100 40 30 25

最后堆中没有元素了,被pop完了。

那我们要建小堆,就需要涉及仿函数。

仿函数的介绍和使用

 创建priority_queue优先级队列的时候,默认使用less这个仿函数。我们也可以手动传入greater仿函数来实现小堆。

这里我要先狡辩一下:为什么传less:建大堆。传greater:建小堆。

注意: 在普通排序的过程中,less是排升序 1 2 3 4 5

                                                greater是排降序 5 4 3 2 1

而在优先级队列中,就很奇怪,我们没办法改变传less:建大堆(降序)。传greater:建小堆(升序),这个事实。我也无法解释,只能记住这个事实。  

我们接下来详细讲解一下什么是仿函数

在C++中,仿函数是一种使用实例化出来的对象来模拟函数的技术。它们通常是通过类实现的,该类重载了函数调用操作符(operator()仿函数可以像普通函数一样被调用,但它们可以拥有状态(即,它们可以包含成员变量,继承自其它类等)

简而言之:一个类,重载()运算符,通过对象调用调用这个()函数。

好处:类模版缺省值可以是一个类,不能是函数。可以把函数指针淘汰了。 

#include<iostream>
class A
{
public:
	A()
	{}

	template<class T>
	const T& operator()(const T& a,const T& b)
	{
		return a + b;
	}
private:
};
int main()
{
	A aa;
	std::cout<<aa(2, 4);
    std::cout<<aa.operator()(2, 4);
    std::cout<<A()(2, 4);
	return 0;
}

分析代码:

  1.  看起来就像调用了aa(int,int);这个函数。实际上:

          aa是A类实例化的对象

          aa(2,4);  是去调用()重载运算符的函数

  2. aa.operator()(2, 4);显示调用

  3.  A()(2,4) 使用了匿名对象

仿函数的一个主要优点是它们可以保持状态,这意味着它们可以在多次调用之间保存和修改信息。这使得它们非常灵活和强大。此外,由于它们是类的实例,它们也可以拥有额外的方法和属性 

greater和less 

std::greater 和 std::less 是预定义的函数对象模板,用于执行比较操作。它们定义在<functional>头文件中。std::greater 用来执行大于(>)的比较,而 std::less 用来执行小于(<)的比较 

函数对象模板 std::less 和 std::greater 的实现通常如下:

namespace std {

//一个less类,并且是struct出来的
template<class T>
struct less {
    bool operator()(const T& lhs, const T& rhs) const {
        return lhs < rhs;
    }
};

//一个greater类
template<class T>
struct greater {
    bool operator()(const T& lhs, const T& rhs) const {
        return lhs > rhs;
    }
};

}

 greater和less的常规用法:

// greater example
#include <iostream>     // std::cout
#include <functional>   // std::greater
#include <algorithm>    // std::sort  algorithm算法库,为了调用sort算法

int main () {
  int numbers[]={20,40,50,10,30};
  std::sort (numbers, numbers+5, std::greater<int>());
  for (int i=0; i<5; i++)
    std::cout << numbers[i] << ' ';
  std::cout << '\n';
  return 0;
}


// less example

int main () {
  int foo[]={10,20,5,15,25};
  int bar[]={15,10,20};
  std::sort (foo, foo+5, std::less<int>());  // 5 10 15 20 25
  std::sort (bar, bar+3, std::less<int>());  //   10 15 20
  if (std::includes (foo, foo+5, bar, bar+3, std::less<int>()))
    std::cout << "foo includes bar.\n";
  return 0;
}

细心的同学就会发现:

 std::sort (numbers, numbers+5, std::greater<int>());

 std::priority_queue(int,vector<int>,greater<int>);

怎么一个有括号,一个没有括号呢?

这就要看它的源码是怎么实现的了。

sort函数:class Compare(类型模版)   -->  Compare comp(对象):函数模板,要传对象(可以是匿名对象)

priority_queue类: class Compare (类模版),传类型,会在优先级队列内部构建一个对象

 二、priority_queue的模拟实现:

基本框架:

#include<vector>
#include<iostream>
#include<list>

using namespace std;
namespace ink {
	template<class T, class Container = vector<T>, class Compare = less<T>>
	class priority_queue
	{
	public:
		void adjust_up(size_t child)
		{}
		void push(const T& x)
		{}
		void adjust_down(size_t parent)
		{}
		void pop()
		{}
		bool empty()
		{}
		size_t size()
		{}
		const T& top()
		{
		}
	private:
		Container _con; //通过这个Container这个类型,构建一个对象
	};
}

🔥push( )

优先级队列里面,我们要插入数据,会进行向上调整

所以实现如下

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

 🔥pop( )

pop需要删除堆顶的数据,我们的方式是首尾交换,尾删,再向下调整

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

 🔥empty( )

直接判断即可

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

 🔥size( )

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

 🔥top( )

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

 接着我们来完成两个关键的函数,向上调整和向下调整

🔥adjust_up( )

 当前位置每次和他的父节点比较

void adjust_up(size_t 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;
		}
	}
}
  • 对于给定的子节点索引child,其父节点的索引计算为(child - 1) / 2
  • 循环条件:while (child > 0)循环确保我们不会尝试移动根节点(因为根节点的索引为0,没有父节点)。循环继续执行,只要当前节点的索引大于0。
  • 完成交换后,更新child变量为原父节点的索引,因为交换后当前元素已经移动到了父节点的位置。然后,对新的child值重新计算parent索引,继绀执行可能的进一步交换
  • 循环终止条件:如果当前节点的值不小于其父节点的值(即堆的性质得到了满足),循环终止,else break;执行

🔥adjust_down( ) 

void Ajustdown(size_t parent)
{
	size_t 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(&a[child], &a[parent]);
			parent = child;
			child = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

 两个调整函数的优化

我上面实现的代码只能完成一种堆的实现,如何进行封装使我们能够根据传参实现大堆或小堆呢?

这里就涉及到仿函数了,注意看我们模版中的第三个参数:

template<class T, class Container = vector<T>, class Compare = less<T>>

 我们首先补充greater和less两个类:

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;
		}
	};

我们控制大小堆,则需要控制两个adjust函数的比较逻辑

仿函数本质是一个类,可以通过模版参数进行传递,默认传的为less,控制它为大堆

template<class T, class Container = vector<T>, class Compare = less<T>>
void adjust_up(size_t child)
{
	Compare com;
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//if (_con[child] > _con[parent])
		//if (_con[parent] < _con[child])

        //这个_con可以是less,也可以是greater,实现比较
		if (com(_con[parent], _con[child]))
		{
			swap(_con[child], _con[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

 com是Compare的对象,它的对象可以像函数一样使用

void adjust_down(size_t parent)
{
	Compare com;
	size_t child = parent * 2 + 1;
	while (child < _con.size())
	{
		//if (child + 1 < _con.size() && _con[child + 1] >_con[child])
		//if (child + 1 < _con.size() && _con[child] < _con[child +1])

        //这个_con可以是less,也可以是greater,实现比较
		if (child + 1 < _con.size() &&com(_con[child],_con[child +1]))
		{
			++child;
		}
		//if (_con[child] > _con[parent])
		//if (_con[parent] < _con[child])
		if (com(_con[parent], _con[child]))
		{
			swap(_con[child], _con[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

对于自定义类型的其他仿函数使用 

 如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}	
	bool operator<(const Date& d)const
   {
	return (_year < d._year) ||
		(_year == d._year && _month < d._month) ||
		(_year == d._year && _month == d._month && _day < d._day);
   }
    bool operator>(const Date& d)const
   {
	return (_year > d._year) ||
		(_year == d._year && _month > d._month) ||
		(_year == d._year && _month == d._month && _day > d._day);
    }
    friend ostream& operator<<(ostream& _cout, const Date& d)
    {
    _cout << d._year << "-" << d._month << "-" << d._day;
     return _cout;
    }
private:
	int _year;
	int _month;
	int _day;
};

void test()
{
	priority_queue<Date, vector<Date>, greater<Date>> pq;

	Date d1(2024, 4, 8);
	pq.push(d1);
	pq.push(Date(2024, 4, 10));
	pq.push({ 2024, 2, 15 });

	while (!pq.empty())
	{
		cout << pq.top() << " ";
		pq.pop();
	}
	cout << endl;
}

 

这是因为调用greater这个仿函数的时候,去比较,涉及自定义类型的比较,就会去调用Date类里面的>运算符重载。

再看下面这个:我如果存的是指针呢?

void test1()
{
	myown::priority_queue<Date*, vector<Date*>, myown::greater<Date*>> pqptr;
	pqptr.push(new Date(2024, 4, 14));
	pqptr.push(new Date(2024, 4, 11));
	pqptr.push(new Date(2024, 4, 15));

	while (!pqptr.empty())
	{
		cout << *(pqptr.top()) << " ";
		pqptr.pop();
	}
	cout << endl;
}

数据就不对了!是因为我们是比较指针的地址,而不是指针指向的内容。

所有我们应该 自己构造一个仿函数:

class GreaterPDate
{
public:
	bool operator()(const Date* p1, const Date* p2)
	{
		return *p1 > *p2;
	}
};

  • 27
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
优先队列(priority_queue)是C++中的一种数据结构,它可以按照元素的优先级进行插入和删除操作。在C++中,优先队列默认使用vector作为底层存储数据的容器,并使用堆算法将vector中的元素构造成堆的结构。优先队列最常用的操作包括push、pop和top。 要遍历优先队列中的元素,可以使用while循环和empty函数来实现。首先,可以使用top函数获取优先队列中的最大(或最小)元素,并将其打印出来。然后,使用pop函数将该元素从优先队列中删除。重复这个过程,直到优先队列为空,即使用empty函数判断优先队列是否为空。在每次循环中,可以将获取的元素打印出来。 下面是一个示例代码来遍历优先队列: ```c++ #include <iostream> #include <queue> using namespace std; int main() { priority_queue<int> pq; pq.push(3); pq.push(8); pq.push(2); pq.push(6); pq.push(9); while (!pq.empty()) { cout << pq.top() << " "; pq.pop(); } cout << endl; return 0; } ``` 以上代码创建了一个优先队列pq,并依次插入了5个元素。然后使用while循环和empty函数遍历优先队列,每次输出堆顶元素,并将其从优先队列中删除,直到优先队列为空。最终输出的结果是按照从大到小的顺序输出了优先队列中的所有元素。 因此,c++优先队列的遍历可以通过循环的方式来实现。在每次循环中,使用top函数获取堆顶元素并打印,然后使用pop函数将其删除。重复这个过程直到优先队列为空。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C++中算法(优先队列、遍历算法、查找算法、排序算法)](https://blog.csdn.net/qq_41915323/article/details/94664541)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [C++——优先级队列](https://blog.csdn.net/qq_55712347/article/details/128874870)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值