文章目录
- 1.设计模式
- 2.stack
- 1.stack的使用
- 1.stack的结构
- 2.stack的接口
- 2.stack的模拟实现
- 1.stack的结构
- 2.接口实现
- 3.queue
- 1.queue的使用
- 1.queue的结构
- 3.queue的接口
- 2.queue的模拟实现
- 1.queue的结构
- 2.接口实现
- 4.了解deque
- 1.deque的原理介绍
- 2.deque的底层结构
- 3.deque的迭代器设计
- 4.deque的优缺点分析
- 5.为什么选择deque作为stack和queue的底层默认容器
1.设计模式
设计模式(design pattern)是对面向对象设计中反复出现的问题的解决方案。这个术语是在1990年代由Erich Gamma等人从建筑设计领域引入到计算机科学中来的。这个术语的含义还存有争议。算法不是设计模式,因为算法致力于解决问题而非设计问题。设计模式通常描述了一组相互紧密作用的类与对象。设计模式提供一种讨论软件设计的公共语言,使得熟练设计者的设计经验可以被初学者和其他设计者掌握。设计模式还为软件重构提供了目标。
总之,设计模式就是为了解决某类重复出现的问题而出现的一套成功或有效的解决方案
设计模式一共有23种,目前我们接触到的设计模式主要就是迭代器模式(Iterator Pattern)。
迭代器模式:迭代器封装之后,提供一种方法顺序访问一个聚合对象中各个元素,不暴露底层细节
那么从本文开始,我们要接触到一个新的设计模式——适配器模式(Adapter Pattern)
适配器模式:适配器实际上是一种转换,将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
2.stack
1.stack的使用
1.stack的结构
可以看到模板参数和我们之前看到的list,vector不同,stack的第二个模板参数是一个容器,在下面的介绍中也能看到,stack是一种容器适配器。本质上就是为这种容器提供一类特定的接口。
2.stack的接口
可以看到,stack的接口比容器的接口少了很多。这些接口使用起来很容易,所以这里就不做示范了。如果需要查看接口的详细信息,这里还是推荐cplusplus.com
接口 | 说明 |
---|---|
stack | 构造一个空栈 |
empty | 判断栈是否为空,返回bool类型 |
size | 返回栈中元素个数,size_t类型 |
top | 返回栈顶元素 |
push | 压栈 |
pop | 出栈 |
2.stack的模拟实现
1.stack的结构
在上文中我们看到stack类模板的模板参数有两个,第一个是存放元素类型,第二个是容器类型,这里容器类型我们与库里面的实现保持统一,默认容器是deque,后文将会介绍deque这个容器
stack这个类中,由于使用了容器,所以在成员变量中,只需要一个容器对象用于存放数据即可,所以结构如下
namespace zht//这里把实现的stack包在命名空间中
{
template<class T, class Container = std::deque<T>>
class stack
{
public:
//成员函数
private:
Container _con;
};
}
2.接口实现
按照上述接口的介绍,我们实现一下上述的重点接口。由于stack是使用了其他容器的,所以我们直接调用该容器的接口即可
//构造函数:我们不显示写,编译器生成。直接调用_con的默认构造
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
const T& top()
{
return _con.back();//适配这类操作的容器都是支持back或者front接口的,直接调用即可
}
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_back();
}
3.queue
1.queue的使用
1.queue的结构
与stack相同,queue也是一个容器适配器,但是和stack的规则不相同,stack是后进先出:LIFO,queue是先进先出:FIFO。
3.queue的接口
和stack很相似的接口,就不一一解释了,如果有需要,可以去cplusplus.com查看详细文档.
2.queue的模拟实现
1.queue的结构
和stack的结构非常相似,代码框架如下:
namespace zht//这里把实现的queue包在命名空间中
{
template<class T, class Container = std::deque<T>>
class queue
{
public:
//成员函数
private:
Container _con;
};
}
2.接口实现
queue的接口实现方式和queue也是非常的类似,唯一需要改的就是push和pop数据的位置
//构造函数:我们不显示写,编译器生成。直接调用_con的默认构造
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
const T& top()
{
return _con.back();//适配这类操作的容器都是支持back或者front接口的,直接调用即可
}
void push(const T& val)//这里的push我们采用尾插的方式插入
{
_con.push_back(val);
}
void pop()//由于push使用的是尾插,所以pop采用头删的方式
{
_con.pop_back();
}
至此,我们就算是能够手撕stack和queue了哈哈。
4.了解deque
我们注意到,stack和queue的容器都是deque。这是为什么呢?在学习数据结构的时候,实现的stack和queue一般是使用顺序表和链表实现的。为什么这里不使用vector和list呢?
vector和list的结构都存在一些缺陷:vector真正的缺点在于扩容消耗,不支持头插头删,因为头部中部删除效率低。而list不支持随机访问,且CPU高速缓存命中率低,空间不连续。而deque兼具了vector和list的优点,虽然deque也有很多缺点,但是作为适配器的容器恰好避免了这些。
对于deque,我们稍微了解一下即可,因为deque合适的使用场景很少。
1.deque的原理介绍
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高 。
这只是逻辑图,deque在底层并不是真的连续空间
2.deque的底层结构
deque的底层是由很多段连续的小空间拼接而成,类似一个二维数组,然后这些小空间的地址存放在一个中控指针数组中。结构示意图如下:
3.deque的迭代器设计
那么deque是如何使用迭代器来维护这个假想的连续结构的呢?
这里参考一下侯捷老师的STL源码剖析中的图:
deque的迭代器包含四个指针,其中cur指向当前值,first和last指向当前cur所在的小段连续空间,node指向中控数组中存放所在连续空间的地址。
4.deque的优缺点分析
优点:头部插入和删除时,不需要搬移元素,效率特别高,扩容时,也不需要搬移大量的元素,因此其效率是比vector高的。与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
缺点:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构
5.为什么选择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的优点,而完美的避开了其缺陷
和queue默认选择deque作为其底层容器,主要是因为:
- stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
- 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。
- 结合了deque的优点,而完美的避开了其缺陷