目录
一、栈的简介
1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作
4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
函数说明 | 接口说明 |
构造空的栈 | |
检测stack是否为空 | |
返回stack中元素的个数 | |
返回栈顶元素的引用 | |
将元素val压入stack中 | |
将stack中尾部的元素弹出 |
#include <iostream>
using namespace std;
#include <stack>
#include "stack.h"
#include "queue.h"
#include "priorityqueue.h"
void test_stack()
{
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
while(!st.empty())
{
cout<<st.top()<<endl;
st.pop();
}
}
二、如何实现一个栈
这里我们写栈的话,采用复用的形式,不再像C语言中那样一点点自己手搓一个栈出来啦
想要查看C语言手搓一个栈出来可以参考下面的博客
对于栈来说有链式栈,也有数组栈,在下面的代码中我们使用的是加入了模板参数的Container,其默认为deque,也就是双端队列。当然我们如果将这个参数设置为list就是一个链式栈,如果设置为vector就是以数组的形式存储的栈。
#ifndef STACK_QUEUETEST_STACK_H
#define STACK_QUEUETEST_STACK_H
#pragma once
//实际上是对vector的简单封装
namespace zhuyuan
{
//加入Container模板参数
//我们的STL底层默认是用deque,也就是双端队列实现的。
//支持头插头删,也支持随机访问
//像是vector和list的合集
template<class T,class Container=deque<T>>
class Stack
{
public:
//插入栈
void push(const T&x)
{
_con.push_back(x);
}
//出栈
void pop()
{
_con.pop_back();
}
//访问栈顶元素
T& top()
{
//访问尾部的数据.back()
return _con.back();
}
const T& top() const
{
//访问尾部的数据.back()
return _con.back();
}
//栈的判空
bool empty() const
{
return _con.empty();
}
//判断当前栈的大小
size_t size() const
{
return _con.size();
}
private:
//封装一个vector
// vector<T> _con;
//任何的类型模板,主要满足上面的那些条件(push等等功能),就可以变成container
Container _con;
};
}
#endif //STACK_QUEUETEST_STACK_H
三、测试代码
#include <iostream>
using namespace std;
//栈和队列都是没有迭代器的接口的,不然这样没办法保持栈和队列的特性
#include <stack>
#include <list>
//适配器是一种设计模式
//栈和队列都是用的容器适配器
#include "stack.h"
#include "queue.h"
#include "priorityqueue.h"
void test_stack()
{
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
while(!st.empty())
{
cout<<st.top()<<endl;
st.pop();
}
}
void test_queue()
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
while(!q.empty())
{
cout<<q.front()<<endl;
q.pop();
}
}
void test_mystack()
{
//底层可以是一个容器,也就是数组栈
// zhuyuan:stack<int,vector<int>> st;
//底层也可以是一个链表,也就是链式栈
zhuyuan:stack<int,list<int>> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
while(!st.empty())
{
cout<<st.top()<<endl;
st.pop();
}
}
int main() {
test_mystack();
return 0;
}
四、deque
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维 数组,其底层结构如下图所示:
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落 在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:
那deque是如何借助其迭代器维护其假想连续的结构呢?
上面图中的参数意义
Cur当前数据
first和last表示buffer的开始和结束
Node反向指向中控位置,方便遍历时找我们的下一个buffer
又是:头尾插入删除,随机访问
(I-第一个buffer中个数)/8 第几个buffer
(I-第一个buffer中个数)%8 那个buffer的第几个
设计缺陷:
1.operator[]计算稍显复杂,大量使用,性能下降(相比vector的[])
2.中间插入删除效率不高
3.底层角度迭代器会很复杂
deque速度测试
void test_op()
{
srand(time(0));
const int N = 100000;
vector<int> v;
v.reserve(N);
deque<int> dp;
for (int i = 0; i < N; ++i)
{
auto e = rand();
v.push_back(e);
dp.push_back(e);
//lt1.push_back(e);
//lt2.push_back(e);
}
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
sort(dp.begin(), dp.end());
int end2 = clock();
printf("vector sort:%d\n", end1 - begin1);
printf("deque sort:%d\n", end2 - begin2);
}
结论
1.Deque真正适合的场景是头尾的插入和删除非常适合,相比vector和list而言。很适合去做stack和queue的默认适配容器
2.中间插入删除多,用list
3.如果随机访问多,用vector
五、什么是适配器
就比方说是我们的手机充电器的转接口,能将220V的电转化为5V1A给我们的手机充电一样,我们的适配器也是同样的原理。
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
上面我们代码中传入的模板参数container也相当于是我们的适配器。
只要这个container满足下面的函数调用条件,就可以使用,无论你是list还是vector,这大大提高了我们的代码的灵活性
也就是有下面的这些功能的Container,都可以作为我们的底层用于存储数据的结构。
六、相关练习
【LeetCode】【最小栈】_桜キャンドル淵的博客-CSDN博客