栈和队列
栈和队列是一种容器适配器,其只允许在容器的两端对数据进行操作,其中
- 栈服从后进先出规则,从栈顶入数据,从栈顶出数据
- 队列服从先进先出规则,从队列尾入数据,从队列头出数据
Stack和Queue的使用
在C++STL库中,其中有容器名为stack和queue,在C语言中我们已经详细解释过栈和队列的用法以及其代码组成,我们直接来看其在C++中的函数接口
Stack
函数说明 | 接口说明 |
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
Queue
函数声明 | 接口说明 |
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
值得一提的是,我们在查询Stack和Queue的模板时,我们会发现其模板组成并非只有数据类型,其在数据类型后还有一个Container
在这里的Container是什么?deque又是什么?
Deque
C++在实现栈和队列时,使用了一个新的数据结构:Deque
我们知道,如果使用vector来实现栈和队列,一旦数据数量多起来,扩容的代价十分巨大。而如果使用list来实现,数据的访问会变得相对困难,那有没有一种数据结构,既能使扩容的代价变小,又可以实现数据的随机访问呢?
此时,便诞生了一种新的数据结构:deque
deque由两部分组成,第一个部分是数据存放区,用很多的数组存储数据,第二部分是中枢数组,用来存储每一个数组的地址
在数据存放区中,每一个数组的大小是固定的,以方便快速计算元素的下标对应的数组
在中枢数组中,数据从中间向两边进行存储,以方便向两边进行扩容
而扩容时,数据存放区不需要变动,只需要对中枢数组进行扩容,然后将地址拷贝到扩容后的数组即可,极大提升了效率
那这么优秀的数据结构,有没有什么弊端呢?
当然,deque在数组中间进行插入和删除操作代价非常大
我们不妨思考,假如有一个已经存放了许多数据的deque,我们需要对其中中间一个位置进行插入,会是什么情况呢?
此时,我们有两种选择
- 像vector一样,将该位置后面的所有元素全部挪动
- 扩容当前数组,只将该数组中后面的元素进行挪动
但是,我们思考两种方法,第一种因为数据并非连续存储,如果想进行移动代价光想想就十分巨大;而对于第二种 ,虽然想象是美好的,但是我们进行该操作时会有一个严重的问题:数组中元素的个数不再对齐,我们在之后进行随机访问时,也就无法计算数据下标对应的元素
所以,deque只在对两端进行数据操作的情况下才表现得优秀
而这恰恰符合栈和队列的要求
模板中的Container
我们再来看,为什么模板中有一个Container呢?
因为我们在文章首就已经说过,栈和队列实际上是一个容器适配器
容器适配器
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
栈和队列就如同以上关系一样,栈和队列虽然也是一个数据结构,但是其是由其它数据结构封装完成,其底层实现可能是vector,可能是list,也可能是deque,而模板中的container便是让我们自由去选择用哪一个底层来实现
由图中我们可以很清楚看到,如果我们不传入container,则其默认使用deque
另外,其实际的代码实现其实很简单
我们在实现时,只需要将栈的操作转换成另一底层数据结构的操作,如果我们使用vector则是vector的push_back,如果使用list则是list的push_back。
这也体现了容器命名统一的好处