👑作者主页:@安 度 因
🏠学习社区:StackFrame
📖专栏链接:C++修炼之路
文章目录
- 一、stack
- 二、queue
- 三、deque
- 四、priority_queue
- 1、仿函数
- 2、实现
如果无聊的话,就来逛逛 我的博客栈 吧! 🌹
stack, queue, priority_queue 都是容器适配器。所谓容器适配器就是封装了一个容器,数据存在容器里,根据要求改造,控制,适配出需要的接口。适配器的本质是一种复用。
一、stack
// Container 为适配器
template<class T, class Container = vector<T>>
class stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
T& top()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con; // 根据模板实例化为链式 or 数组
};
二、queue
实现时适配器默认给为 list 最好。因为 stl 官方库中, pop
接口调用的是 pop_front
,而 vector 没有这个接口(效率不高),实现 pop
接口时,与库统一使用头删,默认使用 list 容器。
由上得,这种实例化方式是不支持的:queue<int, vector<int>>
,因为库中使用的头删,vector 并没有对应接口。
template<class T, class Container = list<T>>
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front(); // vector 没有,库中调用的是 pop_front,所以库不支持 vector
// _con.erase(_con.begin()); // 可以强制适配,不建议,1. vector 头删效率较低;2. 与库中保持一致
}
T& front()
{
return _con.front();
}
T& back()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
三、deque
stack, queue 默认的适配器是双端队列 deque 。
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个动态的二维数组,其底层结构如下图所示:
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:
那deque是如何借助其迭代器维护其假想连续的结构呢?
deque 的缺陷:
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
为什么选择deque作为stack和queue的底层默认容器:
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
- stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
- 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。结合了deque的优点,而完美的避开了其缺陷。
四、priority_queue
优先级队列 priority_queue 使用的默认适配器是 vector ,本质为堆,堆是完全二叉树,为数组形式。其中需要进行大量的 [] ,使用 deque 作为默认适配器效率不高,但是也能用。
优先级队列优先出优先级高的元素,默认大的高,所以默认是大堆,可以通过仿函数控制大小堆。
大堆需要的仿函数 less
,小堆需要的仿函数 greater
,大小相反。
priority_queue<int, vector<int>, less<int>> pq; // priority_queue<int> pq 大
priority_queue<int, vector<int>, greater<int>> pq; // 小
1、仿函数
仿函数看起来像函数名,功能类似函数指针,但是比函数指针的功能更加完善。例如仿函数可以使用类模板,对于各种类型的比较更加便捷。
实际上就是重载了 ()
,对于内置类型直接比较,自定义类型比较的前提是重载了类型的比较符号。
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;
}
};
当然也有这种的比较方式:
优先级队列里存的是指针,需要比较指针指向的内容,所以可以不用模板,写特定类型的比较:
class LessDate
{
public:
bool operator()(const Date* pd1, const Date* pd2)
{
return *pd1 < *pd2;
}
};
2、实现
其他的实现则和之前的堆相似:
namespace lx
{
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;
}
};
template<class T, class Container = vector<T>, class Compare = Less<T>>
class priority_queue
{
private:
// 提供给成员函数的函数,设为私有
void AdjustDown(int parent)
{
Compare com; // 另一种泛型,存储的是比较的类型
int child = 2 * parent + 1;
while (child < _con.size())
{
// if (child + 1 < _con.size() && _con[child + 1] > _con[child])
//
if (child + 1 < _con.size() && com(_con[child], _con[child + 1])) // 2 > 1 就是 1 < 2
{
++child;
}
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
void Adjustup(int child)
{
Compare com;
int parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
public:
priority_queue()
{}
// 迭代器构造
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{
while (first != last)
{
_con.push_back(*first);
++first;
}
// 向下调整建堆:O(N)
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(i);
}
}
void push(const T& val)
{
_con.push_back(val);
Adjustup(_con.size() - 1);
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
size_t size()
{
return _con.size();
}
const T& top()
{
return _con[0];
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};