目录
一、stack
stack的文档介绍:stack - C++ Reference (cplusplus.com)
stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,只能从容器的一端进行 元素的插与提取操作(就是数据结构中的栈)。
stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持这些操作: empty(判空操作)、back(获取尾部元素操作)、push_back(尾部插入元素操作) 、pop_back(尾部删除元素操作)。
标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器, 默认情况下使用deque。
1.1 stack的接口
函数声明 | 接口说明 |
---|---|
stack | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push(const value_type& val) | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
1.2 stack的模拟实现
下面我们使用适配器的方式来模拟实现一个stack:
namespace lhs
{
template<class T,class container>//container为传入的容器类型
class stack
{
public:
void push(const T& val)
{
_st.push_back(val);
}
void pop()
{
_st.pop_back();
}
bool empty()
{
return _st.empty();
}
const T& top()
{
return _st.back();
}
private:
container _st;
};
}
在这个stack中只要传入一个容器类型container,该容器类型内必须要有push_back()、pop_back()、empty()、back()这几个功能函数,这样我们实现的栈可以直接调用这些函数来实现自己的push()、pop()、empty()、top()功能
下面是使用演示:
int main()
{
lhs::stack<int, std::vector<int>> v_st;//顺序栈
lhs::stack<int, std::list<int>> l_st;//链式栈
for (int i = 0; i < 5; ++i)
{
v_st.push(i);
l_st.push(i);
}
for (int i = 0; i < 5; ++i)
{
std::cout << v_st.top()<<" ";
v_st.pop();
}
std::cout << std::endl;
for (int i = 0; i < 5; ++i)
{
std::cout << l_st.top() << " ";
l_st.pop();
}
return 0;
}
但是在STL中stack是不用传容器类型的(STL中stack默认容器类型为deque),我们该怎么办才能做到不声明容器类型呢?
在我们定义模版参数的地方加上一个缺省值即可:
namespace lhs
{
template<class T,class container = std::vector<T>>
class stack
{
public:
void push(const T& val)
{
_st.push_back(val);
}
void pop()
{
_st.pop_back();
}
bool empty()
{
return _st.empty();
}
const T& top()
{
return _st.back();
}
private:
container _st;
};
}
模板参数也可以使用缺省值?是的,你没有看错,下面我们来演示其使用:
int main()
{
lhs::stack<int> st;
for (int i = 0; i < 5; ++i)
{
st.push(i);
}
for (int i = 0; i < 5; ++i)
{
std::cout << st.top()<<" ";
st.pop();
}
return 0;
}
现在在我们不传容器类型的情况下,该stack使用的容器为vector
二、queue
queue的文档介绍:queue - C++ Reference (cplusplus.com)
queue是一种容器适配器,专门用于在FIFO(先进先出)中操作,其中从容器一端插入元素,另一端 提取元素。
queue作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列
底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作: empty(检测队列是否为空) size(返回队列中有效元素的个数 )front(返回队头元素的引用) back(返回队尾元素的引用) push_back(在队列尾部入队列) pop_front(在队列头部出队列)
标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
2.1 queue的接口
函数声明 | 接口说明 |
---|---|
queue | 构造空队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
2.2 queue的模拟实现
下面我们继续使用适配器的方式来模拟实现一个queue:
namespace lhs
{
template<class T, class container = std::list<T>>
class queue
{
public:
size_t size()
{
return _con.size();
}
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_front();
}
bool empty()
{
return _con.empty();
}
const T& front()
{
return _con.front();
}
const T& back()
{
return _con.back();
}
private:
container _con;
};
}
在这里由于queue需要经常进行头删,所以如果使用vector作为其底层适配容器效率是不高的,在这里我们默认queue的底层适配容器为list
测试效果:
int main()
{
lhs::queue<int> Q;
for (int i = 0; i < 5; ++i)
{
Q.push(i);
std::cout << Q.back() << " ";
}
std::cout << std::endl;
std::cout << "size: " << Q.size() << std::endl;
while (!Q.empty())
{
std::cout << Q.front() << " ";
Q.pop();
}
return 0;
}
三、deque
虽然stack和queue中可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque:deque - C++ Reference (cplusplus.com)
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。有着vector和list的所有接口:
3.1 deque底层实现原理
deque为了具有vector和list两者的优势,实现了一种分段存储数据的方法,类似于一个动态的二维 数组,其底层结构如下图所示:
即我们在deque存储的数据存在一个个buff缓冲区中,每一个buff都有一个指针来管理,当存储空间不够时,会再开辟一个buff空间来存储数据,并在中控数组中添加一个指针来管理此区域
3.1.1 头插实现原理
当最前一个buff存满数据时,deque要想实现头插,先要开辟一个buff空间,再在中控数组的最前一个元素前添加一个指针来管理这块空间,最后再将要插入的数据插入到新开辟buff空间的末尾:
当最前一个buff还没存满数据时,deque要想实现头插,直接将要插入的数据插入到最前一个buff空间的最前一个数据的前面:
3.1.2 尾插实现原理
当最后一个buff存满数据时,deque要想实现尾插,先要开辟一个buff空间,再在中控数组的最后一个元素后添加一个指针来管理这块空间,最后再将要插入的数据插入到新开辟buff空间的开头:
当最后一个buff还没存满数据时,直接将要插入的数据插入到最后一个buff空间的最后一个数据的后面:
3.1.3 头删、尾删实现原理
deque要头删时会通过指针数组找到第一个buff空间,再删去其空间内的第一个元素,若删完该元素后buff空间无数据,会释放该空间
deque要尾删时会通过指针数组找到最后个buff空间,再删去其空间内的最后一个元素,若删完该元素后buff空间无数据,会释放该空间
这里不再画图演示
3.1.4 在随机位置插入/删除数据实现原理
由于deque的结构,使得在随机位置插入/删除数据变得很麻烦,这里主要有两种思路:
1、在随机位置插入/删除数据时,挪动插入/删除位置后的所有元素的位置(STL中的实现方式)
2、改变插入/删除位置的buff空间大小(此方法虽然效率比上面一种方法快,但会造成buff空间的不一致导致随机访问deque中元素的效率下降)
3.1.5 访问随机位置数据实现原理
如果deque中每个buff空间大小都相等,将想要访问元素的下标减去最前一个buff空间中元素的个数,再除以buff最大所存储数据的个数,即可得到该元素所在的buff空间,最后将想要访问元素的下标模上buff最大所存储数据的个数,即可得到该元素在buff空间的位置
如果deque中每个buff空间大小不相等,只能将想要访问元素的下标一一减去最每一个buff空间中元素的个数,直到为负时就可以确定该元素的位置
3.2 deque的优缺点
了解到其实现原理后,我们可以发现:
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque的缺陷在于:
不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作 为stack和queue的底层数据结构。
在随机位置插入/删除数据会很麻烦
3.3 deque迭代器实现原理
deque迭代器实现比较复杂:一个指向当前元素的指针cur、一个指向当前buff起始空间地址的指针first、一个指向当前buff结束空间地址的指针last、一个指向当前buff空间的指针node: