好多天没有更新博客了,最近一直在忙着学linux和自己的专业课(因为博主不是科班的~)。现在在学校感觉时间有些紧,等到期末考完了,博主就全身心把linux博客总结并分享给大家~
优先级队列也是STL库中非常实用的一个容器。底层实现和堆很相似,这个容器又和之前讲的string、vector、list...模板参数上有些区别,今天来和老铁们一起见识一下这个容器~
目录
priority_queue的介绍
priority_queue的使用
construct
priority_queue()
priority_queue(first, last)
我们在上面看到了它有三个模板参数,那么我们应该怎么把控呢?
empty()
top()
push()
pop()
size()
swap()
priority_queue模拟实现
Compare仿函数实现
less
greater
priority_queue()
priority_queue(InputIterator first, InputIterator last)
void adjust_down(int parent)
void push(const T& x)
void adjust_up(int child)
void pop()
const T& top() const
size_t size()
bool empty()
完整代码
priority_queue.h
Date.h
测试代码
总结
注意
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。
注意:
默认情况下priority_queue是大堆。
我们先来看一下C++文档的介绍:
我们翻译成中文:
1、优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的(默认是建大堆)。
2、此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
3、优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
4、底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
empty():检测容器是否为空
size():返回容器中有效元素个数
front():返回容器中第一个元素的引用
push_back():在容器尾部插入元素
pop_back():删除容器尾部元素
5、 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
6、 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
无参构造一个空的优先级队列。
举个栗子:
#include
#include
#include
using namespace std;
int main()
{
/*explicit priority_queue(const Compare & comp =Compare(),
const Container & ctnr = Container()); */
priority_queue q;
return 0;
}
我们调试一下看看情况:
通过调试,我们发现这种无参构造就构造出来了一个空的优先级队列。
使用迭代器区间来完成构造初始化,这个迭代器是InputIterator(普通迭代器)。说明我们可以传vector、list...这样的迭代器,只要里面元素类型匹配就行。
举个栗子:
int main()
{
/*template
priority_queue (InputIterator first, InputIterator last,
const Compare& comp = Compare(),
const Container& ctnr = Container());*/
//传vector迭代器
vector v = { 1,2,3,4,5 }; //C++11支持的语法
priority_queue q1(v.begin(),v.end());
//传list迭代器
list l = { 10,20,30,40,50 };//C++11支持的语法
priority_queue q2(l.begin(), l.end());
//普通数组
int arr[] = { 5,4,3,2,1 };
priority_queue q3(arr, arr + 5);
return 0;
}
我们调试一下看看情况:
我们通过调试发现,用vector、list、甚至是数组(传地址,注意地址也是按照左闭右开的规则)都可以来初始化优先级队列!
默认情况下,这个队列是建大堆,所以前两种情况在底层存储时的顺序是和我们传的顺序是有变化的。第三种方式传的时候本身就是大堆,所以底层存储没有变化。
class T:表示存储的元素的类型。
class Container = vector
: 表示底层使用什么容器来实现优先级队列,当我们不传时,默认使用vector。class Compare = less
: 仿函数,我们不传时,默认是建大堆。如果想要建小堆,需要显示指明,并引入头文件 #include
举个栗子:
int main()
{
vector v1 = { 1,2,3,4,5 };
priority_queue, greater> q1(v1.begin(), v1.end());
vector v2 = { 50,40,30,20,10 };
priority_queue, greater> q2(v2.begin(), v2.end());
return 0;
}
我们调试一下看看情况:
priority_queue
, greater 根据我们曾经学习的缺省值传参语法,我们传参时给模板参数从左往右来传。> : 第二个模板参数是我们显示指定使用容器vector<来实现堆>,第三个模板参数的是仿函数,greater这里是指明priority_queue内部实现建立小堆(我们通过监视窗口heap里面的数据建堆就可以发现)。
下面的也是一样~
判断优先级队列是否为空,如果为空就返回真(1);如果不为空就返回假(1)。
举个栗子:
int main()
{
priority_queue q1;
cout << q1.empty() << endl;
vector v = { 1,2,3,4,5 };
priority_queue q2(v.begin(), v.end());
cout << q2.empty() << endl;
return 0;
}
运行结果:
我们发现,q1队列为空,则输出1;q2队列不为空,则输出0。
返回优先级队列中堆顶的数据,实际上就是底层数组的第一个元素。
举个栗子:
int main()
{
vector v = { 1,2,3,4,5 };
priority_queue q(v.begin(), v.end()); //默认是建大堆
cout << q.top() << endl;
return 0;
}
运行结果:
由于底层是建立大堆,5是这几个数据中最大元素,一定是堆顶数据。所以输出了5。
往优先级队列中插入元素val。(插入后可能底层数据关系发生变化,因为要保证堆的规则成立)
举个栗子:
int main()
{
priority_queue q;
q.push(1);
q.push(2);
q.push(5);
q.push(4);
q.push(3);
return 0;
}
我们来调试一下看看情况:
通过监视窗口,我们看到3次push把数据插入到了优先级队列中了。
我们来画图展示这个插入过程,顺便带大家复习一下建堆的知识:
删除优先级队列中堆顶的元素。
举个栗子:
int main()
{
priority_queue q;
q.push(1);
q.push(2);
q.push(5);
q.push(4);
q.push(3);
q.pop();
return 0;
}
我们调试一下来看看情况:
很显然,经过pop()后,堆顶元素5就被删除了,并且同时底层重新建堆。
我们来画图 展示一下这个过程:
返回优先级队列中元素的个数。
举个栗子:
int main()
{
priority_queue q;
q.push(1);
q.push(2);
q.push(3);
cout << q.size() << endl;
return 0;
}
运行结果:
交换两个优先级队列中的数据。
举个栗子:
int main()
{
priority_queue q1;
q1.push(5);
q1.push(4);
q1.push(3);
q1.push(2);
q1.push(1);
priority_queue q2;
q2.push(50);
q2.push(40);
q2.push(30);
q2.push(20);
q2.push(10);
q1.swap(q2);
return 0;
}
我们来调试一下看看情况:
我们通过比较swap前后的数据就可以发现两个优先级队列中的数据是交换成功了。
//仿函数
template
struct less //大堆
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
template<>
struct less
{
bool operator()(const Date*& x, const Date*& y)const
{
return *x < *y;
}
};
//仿函数
template
struct less //大堆。是不是less对应的大堆感觉是有点怪?在实现的时候对应的是小于号。
{
bool operator()(const T& x, const T& y) const //重载(),来实现两个数大小的比较,返回值是bool值,来实现判断大小。
{
return x < y;
}
};//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
template<>
struct less
{
bool operator()(const Date*& x, const Date*& y)const
{
return *x < *y;
}
};
template
struct greater
{
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
template<>
struct greater
{
bool operator()(const Date*& x, const Date*& y) const
{
return *x > *y;
}
};
//greater仿函数实现
template
struct greater //建小堆 ->在下面对应大于号比较
{
bool operator()(const T& x, const T& y) //对()进行重载实现。
{
return x > y;
}
};//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
template<>
struct greater
{
bool operator()(const Date*& x, const Date*& y) const
{
return *x > *y;
}
};
//无参的构造函数
priority_queue()
{}
//无参的构造函数
priority_queue() 可以初始化成一个空的优先级队列
{}
template
priority_queue(InputIterator first, InputIterator last)
:_con(first, last) //先把数据录入_con中,实际上调用了_con的构造函数
{
//建堆
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
{
adjust_down(i);
}
}
//构造函数 -->使用了模板
template
priority_queue(InputIterator first, InputIterator last) //使用迭代器区间进行构造
:_con(first, last) //先把数据录入_con中,实际上调用了_con的构造函数
{
//建堆
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
{
adjust_down(i); 把_con里面的数据进行建堆,在这里采用向下建堆
}
}
void adjust_down(int parent)
{
Compare com;//仿函数对象
int child = parent * 2 + 1;//默认是左孩子
while (child < _con.size()) //_con.size()-1的位置就是最后一个数据下标的位置了。child == _con.size()就会越界!
{
//if (child + 1 < _con.size() && _con[child + 1] > _con[child])
if (child + 1 < _con.size() && com(_con[child], _con[child+1]))
{
child += 1;
}
if (com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void adjust_down(int parent)
{
Compare com;//仿函数对象
int child = parent * 2 + 1;//默认是左孩子
while (child < _con.size()) //_con.size()-1的位置就是最后一个数据下标的位置了。child == _con.size()就会越界!
{
//if (child + 1 < _con.size() && _con[child + 1] > _con[child])
if (child + 1 < _con.size() && com(_con[child], _con[child+1])) //使用仿函数来进行比较,在这里会自动调用com里的operator()。如果使用的是less,里面实现时是用小于号,也就是后面的那个数大,就返回真;
如果使用的是greater,里面实现时是用大于号,也就是前面的那个数大,就返回真;
{
child += 1;
}
if (com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
之前我写过一篇关于常见排序的博客,里面有对堆排序的详细介绍,也包括上面的实现逻辑,在这里我就不再赘述了~
常见排序算法激烈讲解_暴走的橙子~的博客-CSDN博客
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);//插入的数据向上调整
}
void adjust_up(int child)
{
Compare com; //定义一个仿函数对象
int parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void adjust_up(int child)
{
Compare com; //定义一个仿函数对象,方便实现下面两个数据的比较
int parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent], _con[child]))//使用仿函数来进行比较,在这里会自动调用com里的operator()。如果使用的是less,里面实现时是用小于号,也就是后面的那个数大,就返回真;
如果使用的是greater,里面实现时是用大于号,也就是前面的那个数大,就返回真;
{
.........
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
void pop() //删除时,不能直接删除堆顶数据,这样会把原来的堆关系全部打乱。
{
swap(_con[0], _con[_con.size() - 1]); //先交换
_con.pop_back(); //再删除末尾的数据
adjust_down(0); //调整我们再来看看这删除的逻辑:
const T& top() const
{
return _con[0];
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
以上三个函数都是调用了容器_con里面的接口,所以说表面看priority_queue是一个新的STL,实际上还是复用像vector、deque这样的容器~只不过在外面加了一层仿函数的封装以及堆排序算法的逻辑。
#pragma once
#include
#include"Date.h"
#include
#include
#include
using namespace std;
namespace cyq
{
//仿函数
template
struct less //大堆
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
template<>
struct less
{
bool operator()(const Date*& x, const Date*& y)const
{
return *x < *y;
}
};
template
struct greater
{
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
template<>
struct greater
{
bool operator()(const Date*& x, const Date*& y) const
{
return *x > *y;
}
};
template,class Compare=less>
class priority_queue
{
private:
void adjust_down(int parent)
{
Compare com;//仿函数对象
int child = parent * 2 + 1;//默认是左孩子
while (child < _con.size()) //_con.size()-1的位置就是最后一个数据下标的位置了。child == _con.size()就会越界!
{
//if (child + 1 < _con.size() && _con[child + 1] > _con[child])
if (child + 1 < _con.size() && com(_con[child], _con[child+1]))
{
child += 1;
}
if (com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void adjust_up(int child)
{
Compare com; //定义一个仿函数对象
int parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
public:
//无参的构造函数
priority_queue()
{}
template
priority_queue(InputIterator first, InputIterator last)
:_con(first, last) //先把数据录入_con中,实际上调用了_con的构造函数
{
//建堆
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
{
adjust_down(i);
}
}
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);//插入的数据向上调整
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
const T& top() const
{
return _con[0];
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
#pragma once
#include
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
public:
Date(int year=1,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);
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day;
return out;
}
int main()
{
cyq::priority_queue q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
q1.push(5);
vector v = { 50,40,30,20,10 };
cyq::priority_queue q2(v.begin(), v.end());
q2.pop();
cout << q2.top() << endl;
cyq::priority_queue,greater> q4;
q4.push(5);
q4.push(4);
q4.push(3);
q4.push(2);
q4.push(1);
cyq::priority_queue q3;
q3.push(Date(2022, 4, 31));
q3.push(Date(2022, 5, 1));
q3.push(Date(2021, 5, 1));
q3.push(Date(2021, 4, 31));
return 0;
}
通过调试窗口来看:
1、less对应建大堆,也是默认的。 我们在实现仿函数时,内部用小于号 <
greater对应建小堆,我们在实现仿函数时,内部用大于号 >
2、我们要在priority_queue中存放自定义类型时,用户需要在自定义类型中提供> 或者< 的重载。最好也可以把仿函数自己实现,因为有时候像存储Date*这样的类型时,可能会出现问题,例子在下面~(库里面可没有*解引用Date的函数方法实现)
建大堆时:大堆,需要用户在自定义类型中提供<的重载 口诀:大堆->less->小于号
建小堆时:小堆,需要用户提供>的重载 口诀:小堆->greater->大于号
3、表面看priority_queue是一个新的STL,实际上还是复用像vector、deque这样的容器~只不过在外面加了一层仿函数的封装以及堆排序算法的逻辑。
如果老铁们还想进一步了解堆这个算法可以看看博主曾经花了2天写的博客:
常见排序算法激烈讲解_暴走的橙子~的博客-CSDN博客
看下面的代码:
cyq::priority_queue,cyq::greater> q3;
q3.push(new Date(2022, 4, 31));
q3.push(new Date(2022, 5, 1));
q3.push(new Date(2021, 5, 1));
q3.push(new Date(2021, 4, 31));
q3.push(new Date(2021, 6, 31));
q3.push(new Date(2022, 5, 31));
while (!q3.empty())
{
cout << *(q3.top()) << endl;
q3.pop();
}
我们把new出来的对象的地址放进优先级队列。再看看我们实现的push版本:
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);//插入的数据向上调整
}
T在这里自动推导出来的类型是Date*,在这里会走特化的版本(需要自己实现)。
cyq::priority_queue
,cyq::greater >:这里一定要指明cyq::greater 里面的cyq::类域(greater在另一个工程里面,头文件#include 否则greater就走了库里面的了,然后就按照地址进行比较,但是地址是随机的,这样比较显然是不对的!在这里博主调试好久才发现错误的,呜呜~~包括了greater仿函数),
greater的特化版本:
//特化 -- 针对比如日期类这样的自定义类型 ->new出来的Date对象走这个
template<>
struct greater
{
bool operator()(const Date*& x, const Date*& y) const
{
return *x > *y;
}
};
运行结果:
看到这里给博主支持下吧~