priority_queue:优先队列
头文件还是 < queue>
本质就是堆:完全二叉树 + 条件(任意节点都比其孩子大(大根堆))
priority_queue的默认比较是less,但是建出来的是大根堆;sort排序算法用less,得出的是升序
可以发现对于sort和priority_queue,使用greater和less类模板是结果不同的。
- 主要原因是因为priority_queue的内部实现方法是堆,less对应的是大顶堆。在此排序下调用top()得到的是堆顶,也就是取值时是从大到小。push对应的底层函数是push_heap(),每次添加元素入堆时,在默认情况下添加进去的数据作为较小值入堆。
- 顺序真正不同的根本原因
堆的内部实现是vector,每次出堆的时候 实际上是堆顶元素和最后一个元素互换(把最大元素沉到数组末端)。那么实际上假如没有出堆这一个过程而是继续将数据缓存在vector中,所有元素出堆后,形成的数组就是升序的。只是由于出堆这一过程导致了元素出堆反序了,相当于逆序输出。
默认是建大堆:priority_queue<int> first;
如果要建小堆,要给全模板参数
priority_queue<int, vector<int>, greater<int>> third
优先队列如果这么用就显得没有什么必要了,一般来讲都是存储自定义类型,写仿函数作为优先级比较哦
#include<iostream>
#include<queue>
#include<functional>
#include<vector>
using namespace std;
int main()
{
int arr[] = { 10,99,123,213,42,1,7,5 };
sort(arr, arr + (sizeof(arr) / 4),less<int>());
priority_queue<int> q(arr,arr+(sizeof(arr)/4));//模板只传一个参数时,后面两个参数按默认参数来,默认比较方式为less,大根堆 其他的less是升序
priority_queue<int, vector<int>,greater<int>> q2(arr,arr+(sizeof(arr)/4));//当
while (!q.empty())
{
cout << "q.top()=" << q.top() <<"\t" << "q.size()=" << q.size() << endl;
q.pop();
}
while (!q2.empty())
{
cout << "q2.top()=" << q2.top() << "\t" << "q2.size()=" << q2.size() << endl;
q2.pop();
}
return 0;
}
可以给operator<在自定义类中对比较运算符重载,这样默认的模版调用less的<时就会调用这个重载的比较运算符函数,或者在类外定义仿函数,用模版参数传参
/*
template struct less : binary_function <T, T, bool> {
bool operator() (const T& x, const T& y) const { return x < y; }
};
为何要重载 < 运算符,因为pripority_queue的默认参数第三个是less模板类(按升序比较排序),
观察模板可以通过重载 < 运算符来实现自定义类型的比较,也可以重写一个类,在类中重载函数调用运算符 () 仿函数来实现
上面是模板less,将Date替换T,则会出现两个Date对象比较,
因为Date是自定义类型所以要进行<运算符重载才能进行比较,
这样在只传一个Date参数时 priority_queue<Date> q 可以运行,
而要进行大堆创建就得
priority_queue<Date,vector<Date>,greater<Date>> q
并且还要进行 > 运算符重载
*
class Date {
friend class Cmp;
public:
Date(int y, int m, int d)
{
m_year = y;
m_month = m;
m_day = d;
}
void show()
{
cout << this->m_year << "/" << this->m_month << "/" << this->m_day << endl;
}
/*
template <class T> struct less : binary_function <T, T, bool> {
bool operator() (const T& x, const T& y) const { return x < y; }
};
为何要重载 < 运算符,因为pripority_queue的默认参数第三个是less模板类(按升序比较排序),
观察模板可以通过重载 < 运算符来实现自定义类型的比较,也可以重写一个类,在类中重载函数调用运算符 () 仿函数来实现
上面是模板less,将Date替换T,则会出现两个Date对象比较,
因为Date是自定义类型所以要进行<运算符重载才能进行比较,
这样在只传一个Date参数时 priority_queue<Date> q 可以运行,
而要进行大堆创建就得
priority_queue<Date,vector<Date>,greater<Date>> q
并且还要进行 > 运算符重载
*/
bool operator<(const Date& d)
{
if((m_year<d.m_year)||(m_year==d.m_year&&m_month<d.m_month)||(m_day==d.m_day&&m_day < d.m_day))
return true;
return false;
}
bool operator()(const Date& d)const
{
if ((m_year < d.m_year) || (m_year == d.m_year && m_month < d.m_month) || (m_day == d.m_day && m_day < d.m_day))
return true;
return false;
}
private:
int m_year;
int m_month;
int m_day;
};
class Cmp {//在类中重载函数调用运算符 () 仿函数来实现,因为Date类中成员为private所以要在类Date中声明Cmp为friend
public:
bool operator()(const Date& x, const Date& y)
{
if ((x.m_year < y.m_year) || (x.m_year == y.m_year && x.m_month < y.m_month) || (x.m_day == y.m_day && x.m_day < y.m_day))
return true;
return false;
}
};
int main()
{
Date d3(2023, 1, 10);
Date d2(2016, 5, 1);
Date d1(2011, 9, 30);
priority_queue<Date,vector<Date>,Cmp> q;
q.push(d1);
q.push(d2);
q.push(d3);
while (!q.empty())
{
auto x=q.top();
x.show();
q.pop();
}
return 0;
}
优先队列priority_queue,虽然是队列,但是没有front,back方法
反倒像栈一样有个top方法,并且这个top还是const
const value_type& top() const;
模拟实现:
差不多就是用vector封装一个堆,再加上比较器类的仿函数
#pragma once
#include<vector>
namespace gyx
{
template<class T>
struct Less
{
bool operator()(const T& left, const T& right)
{
return left < right;
}
};
template<class T>
struct Greater
{
bool operator()(const T& left, const T& right)
{
return left > right;
}
};
template<class T,class Containter=std::vector<T>,class Compare=Less<T>>
class priority_queue
{
private:
Containter c;
public:
priority_queue(){}
template<class Iterator>
priority_queue(Iterator first, Iterator last)
:c(first, last)//给成员容器进行迭代器构造
{
int last_not_leaf = (c.size() - 2) / 2;//最后一个非叶子结点
for (int i = last_not_leaf; i >= 0; i--)
adjustdown(i);
//对每一个子树进行向下调整,顺序是先从最末尾的根,直到root
}
void push(const T& val)
{
c.push_back(val);
adjustup();
}
void pop()
{
if (empty())
return;
//堆的删除,是把堆顶元素和堆的最后一个元素交换,然后指针前移,再向下调整
std::swap(c.front(),c.back());//这里的front和back是底层vector的方法,不是优先队列的方法
c.pop_back();
adjustdown(0);
}
const T& top()const{ return c.front(); }
int size()const{ return c.size(); }
bool empty()const{ return c.empty(); }
private:
void adjustdown(int parent)//每次传入的都是一个非叶结点
{
//向下调整,先进行
//int size = size();
int child = parent * 2 + 1;
while (child < size() )
{
Compare com;//模版参数传的比较器类仿函数
if (child + 1 < size() && com(c[child], c[child + 1]))
child += 1;
if (com(c[parent], c[child]))
{
swap(c[parent], c[child]);
parent = child;
child = parent * 2 + 1;
}
else return;//说明这个子树是满足堆要求的
}
}
void adjustup()//向上调整是,插入新元素时进行的调整
{
int child = size() - 1;
int parent = (child - 1) / 2;
Compare com;
while (child)
{
if (com(c[parent], c[child]))
{
std::swap(c[parent], c[child]);
child = parent;
parent = (child - 1) / 2;
}
else return;
}
}
};
}
在前面实现stack和queue使用的vecotr和list,但实际stl中是用双端队列deque来实现的
deque
STL的deque是C++标准库中的双端队列容器,提供了快速的在两端插入与删除元素的操作,并且支持随机访问、迭代器等功能。deque采用了分块技术实现,在内存中是由多个连续的定长块组成的,每个块都可以容纳多个元素。
这种实现方式使得deque既能在常数时间内进行头尾插入删除操作,又能以近似常数时间访问任意位置的元素。
- STL的deque采用了一种类似于多个定长数组组成的数据结构,每个数组被称为一个缓冲区(buffer)。这些缓冲区以连续的方式排列在一起,并使用指针来保持它们的连接,从而形成整体的容器结构。
- deque的实现通常会预先分配多个缓冲区,当需要插入新元素时,它会在队列的两端查找邻近的缓冲区并进行操作。如果某一端的缓冲区已满,则可以从另一端的缓冲区中借用一些空间,或者重新分配一些新的缓冲区来扩充容器大小。
- 这种实现方式使得deque拥有了优秀的插入和删除效率,同时也支持快速的随机访问操作,因为我们可以通过指针来直接跳转到任意位置的缓冲区并访问其中的元素。
适用场景:
使用哪种数据结构取决于具体的场景和需求。虽然deque有许多优点,但它也有一些缺点需要考虑。
首先,deque的空间利用率可能不如vector高,因为它存储元素所需的所有缓冲区的大小总和通常会超过实际存储的元素数量,从而导致一定程度的空间浪费。
其次,由于deque的底层结构比较复杂,因此在某些情况下它可能会比其他容器类型产生额外的性能开销。
因此,是否应该使用deque,还需要根据具体应用场景和需求进行选择。如果需要在队列的两端进行高效的插入和删除操作,且同时需要支持随机访问操作,那么deque就是一个不错的选择。不过如果只需要允许在队列头尾进行一端插入和删除操作的话,那么使用更简单的std::queue可能更加合适。