栈和队列的设计模式
相较于前面学习的vector、list都需要自己从0到1去实现,栈和队列就变得非常简单了,只需要在这些容器的基础上封装出栈和队列规则的功能,这种设计模式被称为适配器模式(配接器模式)
适配器模式:该种模式是将一个类的接口转换成客户希望的另外一个接口。
只要是能实现出后进先出的容器,就可以封装成栈(队列同理),就不需要我们自己造轮子了
具体适配器模式如何实现会在模拟实现时介绍
Stack介绍
- stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
- stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
- stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
(1)empty:判空操作
(2)back:获取尾部元素操作
(3)push_back:尾部插入元素操作
(4)pop_back:尾部删除元素操作 - 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
queue介绍
- 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
- 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
- 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
(1)empty:检测队列是否为空
(2)size:返回队列中有效元素的个数
(3)front:返回队头元素的引用
(4)back:返回队尾元素的引用
(5)push_back:在队列尾部入队列
(6)pop_front:在队列头部出队列 - 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
函数说明 | 接口说明 |
---|---|
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
stack模拟实现
适配器模式就是增加一个模板参数,用于适配容器,可以是vector,list等等啥都可以(只要是能实现后进先出的),代码示例如下:
template<class T,class Container>
class stack
{
public:
void push(const T& val)
{
_con.push_back(val);
}
private:
Container _con;
};
使用方法如下:
#include<vector>
int main()
{
stack<int,vector<int>> v1;
v1.push(1);
return 0;
}
上述方法就是适配器模式的设计与使用,可以理解为,容器是自己生产自己卖,适配器是贴牌
注意:这里有个陷阱,stack<int,string> v1; 也可以实现,但是存在很大隐患
(1)string的底层内置类型是基于char的,char也属于整型家族,但当push的单个数据超过256,就会发生截断了
(2)当第一个参数不是整型时,就不能实现了,如double,float
下面来完成栈的模拟实现
template<class T,class Container>
class stack
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_back();
}
size_t size()const
{
return _con.size();
}
bool empty()const
{
return _con.empty();
}
const T& top()const
{
return *(_con.end()-1);
//return _con.back();
}
private:
Container _con;
};
都是在其他容器的基础上套一层壳子但注意,在pop和push后面不可以加const;
const 修饰带表this指针指向的东西都是只读属性,而_con又可以看成里层的vector 的对象,就可以理解为 const vector _con,只能读不能修改,所以这里对修改数据的后面不可以加const
queue模拟实现
自己模拟队列时,不推荐使用vector,并且STL中也不支持队列使用vector,因为队列的规则是先进先出,也就尾插头删,而vector本质上是动态数组,头部操作效率太低,代价太大,并且STL中也暗示了,不提供push_front、pop_front 的函数接口就暗示不支持
template<class T,class Container>
class queue
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_front();
}
size_t size()const
{
return _con.size();
}
const T& front()const
{
return *(_con.begin());
//return _con.front();
}
const T& back()const
{
return *(_con.end()-1);
//return _con.back();
}
const bool empty()const
{
return _con.empty();
}
private:
Container _con;
};
双端队列(deque)
实际使用中发现,使用栈和队列只需要指定一个数据类型,原因就是在定义时给了一个缺省值,缺省值就是deque
template<class T ,class Container = deque<T>>
deque也叫双端队列,是融合了vector和list的优点:
(1)vector:支持随机访问,但是会存在扩容,以及空间浪费,并且接近中部及以上操作效率低
(2)list:按需申请释放空间,头插头删效率同样高,并且任意位置插入删除效率也是 O(1),但不支持下标随机访问
而deque融合了他们的优点,支持头插头删尾插尾删 [ ]访问,并且他的迭代器类型也是随机迭代器,还支持取头尾的数据
如图:
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组
deque的逻辑进行简单的分析如下:
如图所示分析:
相比vector的优势:扩容代价不大,不需要拷贝数据,空间浪费相对不高
相比list的优势:不会频繁的申请小块空间,申请释放次数少代价低,cpu高速缓存命中高
双端队列适合头尾插入删除的应用场景,但中间的操作以及频繁随机访问得效率一般,不如vector和list “各司其职”;所以应用最多的场景就是栈和队列(做为他们的默认容器最合适),其他场合用的并不多。