适配器(adaptor)是标准库中的一种通用概念。容器、迭代器和函数都有适配器。适配器是一种设计模式(设计模式是一套被反复使用的、多人知晓的、经过分类编目的、代码设计的经验的总结),该种模式是一个将类的接口转换成另一个需要的接口。
STL标准库种stack和queue的底层结构:
📗stack和queue也可以存储元素,但STL并没有将它们划分到容器里面,而是将其称为容器适配器,因为stack和queue只是对其它容器的接口进行了包装。
栈是一种容器适配器,专门设计用于后进先出的操作,只能从容器的一端进行插入和提取元素。
stack是作为容器适配器被实现的,它们是使用特定容器的类的封装对象作为其底层容器的类,并提供一组特定的成员函数来访问其元素,元素从特定容器的尾部(栈顶)被压入和弹出。
stack的底层容器可以是任何标准的容器类模板或者是一些其它特定的容器类,这些容器类应该支持以下操作:
🔘empty:判断容器是否为空
🔘back:获取尾部元素的操作
🔘push_back:尾部插入元素的操作
🔘pop_back:尾部删除元素的操作
标准容器vector、deque 和 list 均符合这些要求,默认情况下,若没有为stacj指定特定的底层容器,默认情况下使用deque。
函数说明 | 接口作用 |
---|---|
stack() | 构造一个空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回stack栈顶元素的引用 |
push() | 将value元素压入栈中 |
pop() | 弹出stack栈顶元素 |
stack底层默认用deque实现,若想用 list、vector 实现,只需要用栈定义对象的时候传合适的适配容器即可。
#include
namespace hy
{
//template>
//template>
template<class T, class Container = deque<T>>
class stack
{
public:
stack() {}
void push(const T& val) { _con.push_back(val); }
void pop() { _con.pop_back(); }
T& top() { return _con.back(); }
const T& top()const { return _con.back(); }
size_t size()const { return _con.size(); }
bool empty()const { return _con.empty(); }
private:
Container _con;
};
}
函数说明 | 接口作用 |
---|---|
queue() | 构造空队列 |
empty() | 检测队列是否为空 |
size() | 返回队列中有效元素个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 将value元素压入队列 |
pop() | 将队头元素弹出队列 |
queue的接口存在头删和尾插,用vector来封装效率低下,可以用 list 和 deque 来模拟实现deque。这里我们使用deque作为底层容器。
#include
#include
#include
namespace hy
{
//list
template<class T, class Container = deque<T>>
class queue
{
public:
void push(const T& x) { _con.push_back(x); }
void pop() { _con.pop_front(); }
T front() { return _con.front(); }
T back() { return _con.back(); }
size_t size() { return _con.size(); }
bool empty() { return _con.empty(); }
private:
Container _con;
};
}
函数说明 | 接口说明 |
---|---|
priority_queue() | 构造一个空的优先级队列 |
empty() | 检测优先级队列是否为空 |
top() | 返回优先级队列中最大(最小)元素,即堆顶元素 |
push() | 在优先级队列中插入value |
pop() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
优先级队列默认使用vector作为其底层存储数据的容器,在vector上使用堆算法将vector中元素构造成堆的结构,所以priority_deque就是一个堆,任何需要用到堆的位置,都可以考虑使用priority_queue, priority_queue默认是大堆。
🔎若priority_queue中存放的是自定义类型的数据,在使用的时候我们需要在自定义类型中提供 > 或者 < 的重载函数。
🔎默认情况下,priority_queue是大堆,若要创建小堆的priority_queue我们需要将第三个模板参数换成greater的比较方式。
#include
#include
#include
void TestPriorityQueue()
{
// 默认情况下,创建的是大堆,其底层按照小于号比较
vector<int> v{ 1,3,8,2,0,6,4,5,9,7 };
priority_queue<int> pq;
for (auto& e : v)
pq.push(e);
while (!pq.empty())
{
cout << pq.top() << " "; // 9 8 7 6 5 4 3 2 1 0
pq.pop();
}
cout << endl;
//若要创建小堆,将第三个模板参数换成greater比较方式
priority_queue<int, vector<int>, greater<int>> pq1(v.begin(), v.end());
while (!pq1.empty())
{
cout << pq1.top() << " "; // 0 1 2 3 4 5 6 7 8 9
pq1.pop();
}
cout << endl;
}
int main()
{
TestPriorityQueue();
return 0;
}
📚priority_queue的底层结构是堆,所以要模拟实现 priority_queue 我们需要先学会建堆和堆的删除的算法。以大堆为例来看一下。这里也有详解:二叉树初级
例如:插入一个值为76的数据进堆,然后进行向上调整,直到满足堆的结构。
堆的向上调整算法:
//堆的向上调整算法
void AdjustUp(vector<int>& v,size_t child)
{
size_t parent = (child - 1) / 2; // 算出child父亲的下标
while (child > 0 && v[child] > v[parent]) // 若父亲大于孩子且孩子的下标大于0,那么继续调整
{
swap(v[child], v[parent]);
child = parent;
parent = (child - 1) / 2;
}
}
堆的删除是删除堆顶的数据,将堆顶的数据与最后一个数据交换,然后删除最后一个数据,进行堆的向下调整算法。
堆的向下调整算法:
//堆的向下调整算法
void AdjustDown(vector<int>& v,size_t parent,int n)
{
// child记录左右孩子中较大孩子的小标
size_t child = parent * 2 + 1; // 默认左孩子较大
while (child < n)
{
// 若右孩子存在且比左孩子大,让child改为右孩子下标
if (child + 1 < n && v[child + 1] > v[child])
++child;
// 较大孩子的值大于父亲
if (v[parent] < v[child])
{
swap(v[parent], v[child]); // 交换节点
parent = child;
child = parent * 2 + 1;
}
else
{
// 满足条件,停止交换
break;
}
}
}
注意:向上和向下调整算法中的仿函数比较,传的参数位置不要传反了,否则构建出来的堆结构不正确。
namespace hy
{
//仿函数的使用,使传模板参数时,可以根据传的比较方式进行灵活的比较
template<class T>
struct less
{
bool operator()(const T& l, const T& r)const
{
return l < r;
}
};
template<class T>
struct greater
{
bool operator()(const T& l, const T& r)const
{
return l > r;
}
};
//typename的存在是告诉编译时,后面是一个类型名称,需要等后面类模板实例化以后再去里面找这个value_type
template<class T, class Container = vector<T>, class Compare = less<typename Container::value_type>>
class priority_queue
{
public:
//创造空的优先级队列
priority_queue() :_con() {}
//堆的向上调整算法
void AdjustUp(size_t child)
{
Compare com;
size_t parent = (child - 1) / 2; // 算出child父亲的下标
while (child > 0 && com(_con[parent], _con[child])) // 若父亲大于孩子且孩子的下标大于0,那么继续调整
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
}
//堆的向下调整算法
void AdjustDown(size_t parent)
{
Compare com;
size_t child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
++child;
}
if (com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void push(const T& val)
{
_con.push_back(val);
AdjustUp(_con.size() - 1);
}
void pop()
{
if (empty()) //若为空,直接返回
return;
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
//堆顶元素不能修改,会破坏堆的结构
const T& top()const
{
return _con[0];
}
bool empty()const
{
return _con.empty();
}
int size()const
{
return _con.size();
}
private:
Container _con;
};
}
deque 底层是一段假象的连续空间,实际上是分段连续的,为了维护其”整体连续”以及随机访问的假象,落在了 deque 的迭代器上,因此 deque 的迭代器设计复杂。具体可以参考侯捷老师的《STL源码剖析》。
vector :
优点 | 缺点 |
---|---|
a . 适合尾插尾删,随机访问 | a.不适合头部或者中部插入删除,需要挪动数据,效率低 |
b. CPU高速缓存命中率高 | b.扩容有一定的性能消耗,可能存在一定程度的空间浪费 |
list :
优点 | 缺点 |
---|---|
a.任意位置插入删除元素效率高(O(1)) | a.不支持随机访问 |
b.可按需申请空间,不浪费空间 | b.CPU高速缓存命中率低 |
deque :
优点 | 缺点 |
---|---|
a.头部和尾部插入数据效率高,支持随机访问 | a.中间部位插入删除数据效率低 |
b.扩容代价小,CPU高速缓存命中率高 | b.虽支持随机访问,但相较于 vector 而言效率还是有差距 |
🔖deque的缺陷:不适合遍历,在遍历时,deque 的迭代器要频繁检测其是否移动到某段小空间的边界,导致效率低下,在序列式场景中,可能经常需要遍历。在实际中,需要线性结构时,大多数情况下优先考虑 vector 和 list ,deque 的应用场景不多,在STL中用其作为 stack 和 queue 的底层数据结构。
🖍️stack 和 queue 不需要遍历,只需要在固定的一端或者两端进行插入和删除等操作。
🖍️stack 中元素增长时,deque 比 vector 的效率高,因为不需要挪动大量数据;queue 中的元素增长时,deque 不仅效率高,且内存的使用率高。