c++优先级队列(priority_queue)及仿函数

1. 优先级队列

1.1. 介绍

priority_queue是一种容器,被称为 优先级队列。
虽然名字中有队列,但是和之前学的先进先出的队列不一样,优先级队列是优先级比较高的先出。
在这里插入图片描述

  1. 优先级队列也是一个容器适配器,一般是用vector适配生成的。
  2. 名字有队列,其结构和堆很像,优先级高的先出,对一组数来说优先级高的默认是数字大的,也就是传入一组数,大的先出。
  3. 这个容器是放在 queue 的头文件中,所以要使用这个容器,必须先写 #include

1.2. 简单使用

根据上面 priority_queue 的介绍,其函数并不是很多,大部分和之前学的区别不大。

#include<iostream>
#include<queue>
using namespace std;
void test_priority_queue()
{
	priority_queue<int, vector<int>> p1;
	//这里的vector<int>可以不传,priority_queue的默认容器模版就是vector<int>
	p1.push(10);
	p1.push(20);
	p1.push(30);
	p1.push(40);
	p1.push(50);
	p1.push(60);
	while(!p1.empty())
	{
		cout << p1.op() <<"  ;
		p1.pop();
	}
	cout << endl;
}
int main()
{
	test_priority_queue();
	return 0;
}

在这里插入图片描述
在运行上面代码后,我们可以发现,我们传入的原本是 10 到 60 ,但是输出结果是 60 到 10
优先级高的先出,对这些数来说,默认大的数据是优先级高的,所以输出就按照从小到大的顺序输出。
同样,这里也不支持使用迭代器,所以我们不能像string那样用迭代器访问优先级队列,我们只能参考出队列的方式输出元素。
改变优先级
如果我们想要让小的数据优先级高,和前面学的 sort 一样,我们需要传入 greater(这里先知道这样用,下面仿函数哪里会说到为什么)

priority_queue<int, vector<int>, greater<int>> p1;

在这里插入图片描述
可以看到此时小的数优先级更高。
这里和排序是反着来的
priority_queue使用了 vector 作为自己的默认容器,除了这个容器,我们也可以传入 deque 容器类型。

2. 模拟实现优先级队列

这里模拟实现 priority_queue 的主要功能。

1. 成员变量

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

首先优先级队列我们可以用堆来实现,而堆的底层一般是一个顺序表,所以我们这个类,主要是对顺序表构造出来的对象进行操作。
这里只是默认是 vector ,传入其他容器类型也可以。

2. push_back

尾插
和之前学的堆一样,结构上可以类比成二叉树的模式,但是底层还是顺序表结构。
想象中的结构:
在这里插入图片描述
实际的结构:
在这里插入图片描述
和堆的尾插一样,在顺序表的最后一个位置插入元素,然后不断调整,直到新元素调整到正确的位置。
这里要牢记父节点下标和孩子节点下标之间的关系
parent = (child - 1) / 2;

void adjust_up(int child)
{
	int parent = (child - 1) / 2;
	while(child > 0)
	{
		if(_con[child] > _con[parent])
		{
			std::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());
}

在这里插入图片描述
孩子节点和父节点比较,小堆的话小的值不断往上走,直到插入的值小于父节点的值,大堆与此相反即可。

3. pop

删除元素
这里要注意的是,优先级队列只能删除堆顶的元素,不能想删哪个删哪个,和队列只能出第一个元素一样。
如果是顺序表,我们想要删除第一个元素很麻烦,需要一个一个向前覆盖,而且覆盖后也不能保证新的结构是堆。
这里我们采用的方式是 首元素 和 最后一个元素 交换,然后删除最后一个元素(原本的首元素),然后把此时的首元素(原来最后一个元素)向下调整到正确为止。
在这里插入图片描述
这里创建的是小堆,需要注意的是,父亲节点的值必须小于两个子节点,所以向下调整的时候需要判断哪个孩子节点最小,然后和父节点互换。

void adjust_down(int parent)
{
	int child = parent * 2 + 1;
	while(child < _con.size*())
	{
		if( child +1 <_con.size() && _con[child + 1] > _con[child])
		{
			child = child+1;
		}
		if(_con[parent] < _con[child])
		{
			std::swap(_con{parent], _con[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void pop()
{
	swap(_con[0], _con[_con.size() - 1]);
	_con.pop_back();	
	adjust_down(0);
}

4. 其他函数

const T& top()
{
	return _con[0];
}
bool empty()
{
	return _con.empty();
}
size_t size(0
{
	return _con.size();
}

返回堆顶元素,判断是否为空,返回元素个数
大部分容器都支持这三个函数,所以我们直接调用容器对象的对应函数即可。

5. 关于构造,析构,拷贝构造

priority_queue 是使用的容器模版,当使用这个类创建对象,或者销毁对象的时候,因为他是自定义类型,所以会自动调用内部成员的构造、析构和拷贝构造,所以我们模拟实现的时候不需要写它的这三个默认构造函数

void test_priority_queue()
{
	xsz::priority_queue<int,vector<int>> q1;
	q1.push(1);
	q1.push(14);
	q1.push(56);
	q1.push(3);
	q1.push(6);
	xsz::priority_queue<int> q2(q1);
	while(!q1.empty())
	{
		cout << q1.top() << "  ";
		q1.pop();
	}
	while(!q2.empty())
	{
		cout << q2.top() << "  ";
		q2.pop();
	}
}

在这里插入图片描述
在我们没写拷贝构造的情况下,是可以正常使用的

3. 仿函数

3.1. 什么是仿函数

先看一段代码

class Less
{
public:
	bool operaetor()(int x, int y)
	{
		return x < y;
	}
};
int main()
{
	Less less;
	cout << less(2,3) << endl;
	return 0;
}

这里我们先有一个 Less 的类,在主函数中使用 Less 创建了 less 的对象,因为 Less 实现了 “()” 的运算符重载,所以创建出来的对象可以直接使用 “()”。
这样使用看起来好像是一个函数(函数名,括号里是参数),因此这种方法被叫做仿函数。
运算符重载后,本来我们应该这样写

cout << less.operator()(2,3) << endl;

实际上两种写法都可以,只不过上面仿函数的写法更简单。
注意:只要类中重载了 “()” 的运算符,使用起来都可以叫做仿函数,这里的 “()” 不能换成其他符号 “{}” “[]” 都是不行的。

3.2. 简单使用

仿函数因为涉及类,所以也可以和模版结合起来使用,比如:

template<class T>
class Less
{
public:
	bool operator()(T x, T y)
	{
		return x < y;
	}
};
int main()
{
	Less<int> less1;
	cout << less1(2, 3) <, endl;

	Less<double> less2;
	cout << less2(2.1, 3.2) << endl;
	return 0;
}

有了模版之后,我们就能通过创建不同类型的对象,来出来多组不同类型的数据。
仿函数出现,可以替代函数指针
修改堆
上面我们简单模拟实现了优先级队列,也就是堆,但是这写代码是固定的,也就是要么小堆,要么大堆,如果想将大堆改小堆,就必须改代码中的大于小于号,这样在实践中很麻烦。
在c 语言中,我们采用的方法是 使用函数指针来解决
而这里仿函数也可以解决这个问题。

template<class T>
class Less
{
public:
	bool operator()(T x, 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)
	{
		Compare com;
		size_t parent = (child - 1) / 2;
		while(child > 0)
		{
			if(com(_con[parent], _con[child))
			{
				std:;swap(_con[child], _con[parent]);
			}
			else
			{
				break;
			}
			child = parent;
			parent = (child - 1) / 2;
		}
	}
	//...
};

我们将像上调整的的代码稍作修改,模版中加入了新的类模版,Compare,默认是 Less,在向上调整的函数中,用 Less 模版创建对象,然后用 Less 的小于在 if 中对两个数进行比较。
如果此时我们不想要用 小于,都改成大于呢

template<class T>
class Greater
{
public:
	bool operator()(T x, T y)
	{
		return x > y;
	}
};

加上这段代码,当我们传入参数写成下面的方式时

xsz::priority_queue<int,vector<int>,Greater<int>> q1;

这样向上调整的时候,创建的 Compare 都会是 Greater 创建出来的对象,在比较的时候都会使用大于。
c++库中就是通过实现 less 和 greater 的方式,使这里优先级队列的“优先级”受敲代码的人的控制,而不需要去底层改代码。
c++ priority_queue 传入 less 和 greater :
在这里插入图片描述
在这里插入图片描述
注意函数和模版的区别
库中实现的 sort, 默认情况下是升序

int main()
{
	int a[] = { 3, 5, 2, 3, 1, 5, 6, 7, 8);
	sort(a, a + 9);
	for(int i = 0; i < 9; i++)
	{
		cout << *(a + i) << "  ";
	}
	return 0;
}

在这里插入图片描述
在这里插入图片描述
而在库中,sort传入的参数有一个 “Compare comp”
这里的 comp 就和上面传入 less ,greater 类似
我们传入这个参数试试

sort(a, a + 9, greater<int>);

在这里插入图片描述
我们发现,这样写编译器直接报错了。
当我们这样写才能通过

sort(a, a + 9, greater<int>();

在这里插入图片描述
在这里插入图片描述
这里要注意 模版参数 和 函数参数 的区别
模版参数,传入了对应的类型,就能往后走。
函数参数,需要有个实参,传过去后有形参接收实参的值
这里我们是创建了一个匿名对象,将匿名对象传给了 sort 函数。

3.3. 比较指针指的内容

仿函数除了上面直接对比之外,也可以对指针进行对比
先看下面代码

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);
private:
        int _year;
        int _month;
        int _day;
};

ostream& operator<<(ostream& _cout, const Date& d)
{
        _cout << d._year << "-" << d._month << "-" << d._day << endl;
        return _cout;
}
int main()
{
        priority_queue<Date> q1;
        q1.push(Date(1111, 2, 2));
        q1.push(Date(2222, 3, 3));
        q1.push(Date(3333, 4, 4));
        cout << q1.top() << endl;
        return 0;
}

上面简单实现了一个日期类的 构造 和 比较,还有个输入
主函数中先创建了 q1 这个优先级队列,元素类型是Date ,后面插入了三个元素,然后让最大的输出
在这里插入图片描述
这里可以运行,完全没什么问题
如果我们元素类型是指针怎么办?

        priority_queue<Date*> q1;
        q1.push(new Date(1111, 2, 2));
        q1.push(new Date(2222, 3, 3));
        q1.push(new Date(3333, 4, 4));
        cout << *(q1.top()) << endl;

这次出来的结果是随机的
在这里插入图片描述
在这里插入图片描述
优先级队列中保存的不再是日期,而是指针,不管是什么类型的指针,都是指针,都算是内置类型。
当我们传入指针的时候,类中比较的就不再是输入的日期大小,而是比较指针的大小,因为我们只写了比较值的函数,没有写比较指针指向内容的函数。
可不可以函数重载一下呢?
这里不可以,因为指针属于内置类型,内置类型不能运算符重载。
所以我们还是采用仿函数的形式

class PDataCompare
{
public:
	bool operator()(const Date* x, const Date* y)
	{
		return *x < *y;
	}
};
xsz::priority_queue<Date*, vector<Date*>, pDateCompare> q2;

有了上面可以比较指针的类,我们直接传入这三个模版参数(这里的模版参数是 priority_queue 的模版参数,和 Date 无关,Date 类不需要修改)
在这里插入图片描述

这样,在每次比较元素类型的时候,模拟实现的队列会先使用比较指针的类创造对象,就能通过改变输入从而改变结构,不需要我们去改底层代码

  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值