💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
文章目录
- 前言
- 一、priority_queue的介绍
- 二、priority_queue的具体使用
- 三、模拟实现
- 3.1priority_queue的框架
- 3.2构造函数
- 3.3push和pop
- 3.4其他功能函数
- 3.5完整代码
- 四、仿函数
- 五、总结
前言
今天我们来学习优先级队列,说他是优先级队列,不如叫他优先级栈,因为他的接口和栈的几乎一样,插入和删除实际都是在栈顶操作,只是在插入和删除的时候进行一些数字的调整,那哦我们一起来看看什么是优先级队列。
一、priority_queue的介绍
我们看到优先级队列也是一个容易适配器,默认的是vector,一会再说为什么使用vector,仿函数就是为了控制以什么优先方式出队列,
我们再来看看有哪些接口:
大家看到我一开始为什么说叫做优先级栈了吧,因为接口和栈的几乎一模一样。
我们来看看构造函数:
我们看到构造函数有两种,一个默认构造,一个是传其他迭代器的首尾进行构造
具体有什么功能我先不说,等具体使用的时候就知道了
二、priority_queue的具体使用
我们来看看这几个接口的功能:
priority_queue<int> pq;
pq.push(3);
pq.push(2);
pq.push(4);
pq.push(1);
cout << pq.size() << endl;
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
我们发现我们插入的时候不是有序的插入,为什么最后是降序的打印出来呢?虽然接口和栈一样,但是功能却是不一样的。
大家思考一件事: 我们的优先级队列默认的底层容器是vector,也就是顺序表的结构,我们想要尾插进去的数字进行排好序,那么最好的办法就是向上调整算法,使他变成一个大/小堆,但是最大/最小的元素在第一个位置,我们的vector没有头删的接口,所以需要把第一个位置和最后一个进行交换,然后删除最后一个元素,使他继续变成大堆/小堆,就是使用向下调整算法,所以有了这个猜想,我们的优先级队列就好比之前的堆排序。
按照刚才的运行结果,我们发现使降序,说明我们的优先级队列默认里面是建大堆的,也说明我们如果使用第二个构造函数进行初始化,就要在构造函数里面进行建堆操作,因为要建堆,所以我们需要大量的使用[],而之前说到deque虽然也能支持随机访问,但是效率没有vector高,所以我们默认使用的vector容器
三、模拟实现
在有了前面的简单说明后,我们就来模拟实现一下,有了之前模拟实现的经验,我们需要自己的命名空间域,然后再模拟实现,不记得建堆的来看这篇博客【数据结构】-堆排序以及时间复杂度的计算
3.1priority_queue的框架
namespace xdh
{
template<class T,class Container = vector<T>>
class Priority_queue
{
public:
Priority_queue(){}//默认构造器
private:
Container _c;//容器适配器
};
}
3.2构造函数
我们主要实现第二个构造函数,我们来看看:
template<class InputIteartor>
Priority_queue(InputIteartor first, InputIteartor last)
{
while (first != last)//先把数据插入到vector里面
{
_c.push_back(*first);
first++;
}
for (int i = (_c.size() - 1 - 1) / 2; i >= 0; i--)//通过向下调整算法进行建堆,这个再堆那篇博客说到过
{
AdjustDown(i);
}
}
3.3push和pop
我们再push的时候要保证整体还是一个堆,就要进行向上调整算法,我们是通过把最后一个元素个堆顶的元素进行交换之后,然后通过向下调整算法使其还是一个堆,我们知道我们再进行向下调整肃反的时候,不能把第一个元素给删除了,然后把数据让前面挪一位,再进行调整,这样效率不高,而且父子结点的下标都乱了,这也刚好说明我们的优先级队列选择了这个没有头删接口的vector
void push(const T&val)
{
_c.push_back(val);
AdjustUp(_c.size() - 1);
}
void pop()
{
if (_c.empty())
{
return;
}
swap(_c.front(), _c.back());
_c.pop_back();
AdjustDown(0);
}
为什么要这样建堆,我就不做详细介绍了,参考上面的博客
3.4其他功能函数
T& top()
{
return _c.front();
}
size_t size()const
{
return _c.size();
}
bool empty()const
{
return _c.empty();
}
3.5完整代码
namespace xdh
{
template<class T,class Container = vector<T>
class Priority_queue
{
public:
Priority_queue(){}
template<class InputIteartor>
Priority_queue(InputIteartor first, InputIteartor last)
{
while (first != last)
{
_c.push_back(*first);
first++;
}
for (int i = (_c.size() - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(i);
}
}
void push(const T&val)
{
_c.push_back(val);
AdjustUp(_c.size() - 1);
}
void pop()
{
if (_c.empty())
{
return;
}
swap(_c.front(), _c.back());
_c.pop_back();
AdjustDown(0);
}
T& top()
{
return _c.front();
}
size_t size()const
{
return _c.size();
}
bool empty()const
{
return _c.empty();
}
private:
void AdjustDown(int parent)
{
Compare com;
int child = parent * 2+1;
while (child<_c.size())
{
if (child + 1 < _c.size() && _c[child] ,<_c[child + 1])
{
child += 1;
}
if ( _c[parent]<_c[child] )
{
swap(_c[child], _c[parent]);
parent=child;
child = parent*2+1;
}
else
{
break;
}
}
}
void AdjustUp(int child)
{
Compare com;
int parent = (child - 1) / 2;
while (child >= 0)
{
if (_c[parent]< _c[child])
{
swap(_c[child], _c[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
private:
Container _c;
};
}
四、仿函数
这里面的我们只能简单的做一个了解,后面再细讲,先带大家入个门,让大家熟悉一下。如果大家看过我们的博客,我在vector的具体使用那一篇中提到仿函数了,哪个时候就说一下,今天我们来实现一下
我们的仿函数也是我们STL的六大组件之一,可以说他和迭代器哪些都是平起平坐,也说明他给我们的使用带来了很大的方便,我们先参考这篇博客C/C++编程:仿函数,这里面说到比较细致,我就针对优先级队列来说一下仿函数怎么使用
我今天降的知识仿函数中的关系运算中的其中两种,一种是less,还有一种是greater,一个小于运算符,一个是大于运算符,通过上面的博客我们知道,我们就是将操作放在一个函数对象里面,通过调用操作去使用,我们上面默认是less,建大堆,其实建什么堆是根据孩子结点和父亲结点的大小去决定的,我们将>或者<封装再一个类里面,然后通过创建对象,去调用不就行。我们来看实现:
template<class T>
struct Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
struct Greater
{
public:
bool operator()(const T& x, const T& y)//操作封装再类里面
{
return x > y;
}
};
然后哪里需要用就创建对象,我们刚才就在向上调整算法和向下调整算法里面用到了比较,我们修改后的算法为:此时我们优先级队列就有了第三个模板参数
void AdjustDown(int parent)
{
Compare com;
int child = parent * 2+1;
while (child<_c.size())
{
if (child + 1 < _c.size() && com(_c[child] , _c[child + 1]))
{
child += 1;
}
if ( com(_c[parent],_c[child]) )
{
swap(_c[child], _c[parent]);
parent=child;
child = parent*2+1;
}
else
{
break;
}
}
}
void AdjustUp(int child)
{
Compare com;//创建函数对象
int parent = (child - 1) / 2;
while (child >= 0)
{
if (com(_c[parent], _c[child]))//进行调用
{
swap(_c[child], _c[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
注意:我们的仿函数里面的比较针对自定义类型的话,自定义类型必须重载我们对应的比较运算符,不然会报错。
注意如果传的是对象的引用,那么比较的时候就要解引用用对象去比较,不能用引用,因为引用时指针,会当成内置类型一样去比较,而且每次运行的指针类型都是不一样的,所以我们每次排出来的都可能使不一样的,所以我们要注意,针对下面的就必须使用下面这种仿函数的写法了
template<class T>
struct LessPNode
{
public:
bool operator()(const T x, const T y)
{
return *x < *y;
}
};
template<class T>
struct GreaterPNode
{
public:
bool operator()(const T x, const T y)
{
return *x > *y;
}
};
Priority_queue<Date*,vector<Date*>, GreaterPNode<Date*>> pq;
pq.push(new Date(2018, 10, 29));
pq.push(new Date(2018, 5, 28));
pq.push(new Date(2018, 7, 30));
while (!pq.empty())
{
cout << *(pq.top()) << " ";//因为vector里面存放的使引用是地址,要解引用才能取出来数据
pq.pop();
}
cout << endl;
五、总结
相信大家堆上面的知识应该了解了吧,优先级队列的实现并不难,理解起来我感觉比list简单多了,对于仿函数大家现在知道就行了,以后用到了再说,我们今天的知识就分享到这里了,我们下一篇讲解反向迭代器,因为讲了容器适配器,才能刚好的讲解这个反向迭代器,我们下篇再见