💻前言
关于 stack 和 queue,这两个容器,应用十分广泛,而且常见的实现方式是链式和顺序形式,也就是带有一定限制的顺序表或者是链表。按照我们之前C语言的思维,我们要先手搓“轮子”。然后在轮子的帮助下我们才能完成具体是实现,但是在c++中则不一样了,因为c++里多了stl库,那些已经封装好的数据结构是我们实现他们的一大助力
stack
简介
如果想讲清楚它的实现我们就要对其有一定的了解,才能从底层角度来对其进行一定的模拟,那么让我们来看一下对应的文档
在图片中我们可以清楚的了解到stack的特性它是符合"LIFO"(后进先出)原则的栈,然后往后阅读,我们会发现它是一个 用adaptor (适配器)进行实现的一个容器,它和其他容器的不同之处在于,它除了显式指定类型的类模板参数之外还多了个Container 参数,这个参数,我们还能看到对应的缺省参数,是一个显式指定类型的deque 容器。所以不难猜测,这个参数是一个,传入容器的参数,那它为什么要设计这个模板参数呢?谈及这点,我们就不得不对我们上文一个名词进行解释——adaptor(适配器)
适配器
说到适配器,我们第一时间想到的就是电源适配器也就是,手机充电器,它的最大功能就是转换,将本来不适合用的转换成适合用的:
那么我们这里谈到的stl容器的适配器,其实思想一样,就是将本来不是的东西,通过一些手段将已有的容器改造对其进行适配,来符合我们现阶段要求的特征,stack其实是一种适配器实现,并不是切切实实的自己实现,所以它的实现难度就大幅度降低了。故适配器与其说是一种语法,倒不如说是一种复用的智慧。
接口
既然知道了实现思想,那我们再来看看一些核心接口:
我们发现这些接口其实都是较为简单的,而且值得注意的是我们并不用实现迭代器,因为无论是栈还是队列都是不支持遍历访问的,所以并不需要。
那么我们接下来对这些接口进行一定的使用和测试
#include<stack>
#include<iostream>
using namespace std;
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();
st.pop();//我觉得在pop的同时进行top的返回倒是不错的设计。
}
//因为不能遍历所以不能用范围for进行相应的操作。
}
int main()
{
test_stack();
return 0;
}
我们通过测试我们可以发现,stack的pop并没有返回top,这对我们的实现有了一大助力。
代码实现
#include<iostream>
#include<vector>
#include<list>
using namespace std;
//其实stack和队列只不过是对一些容器进行的应用和调用
namespace yxt
{
//因为是适配器所以要传入适配的容器
template<class T,class container = vector<T>>//默认是vector,模板也能给默认参数进行缺省只不过是类型罢了
class stack{
container _stack_;
//并不需要写构造函数,用编译器自动成成的就能满足需求,因为自定义类型的话会回调默认构造函数
public:
void push(const T& val)
{
_stack_.push_back(val);
}
void pop()
{
_stack_.pop_back();
}
T& top()
{
return _stack_.back();
}
bool empty()
{
return _stack_.empty();
}
size_t size()
{
return _stack_.size();
}
void swap(stack<T>& s_swap)
{
std::swap(_stack_,s_swap._stack_);
}
};
可以发现的是我们并没有对stack的底层怎么自己实现就将stack写了出来,虽然只有一些核心接口但是已经够用了。值得注意的是我这里并没有用dequeue默认缺省,因为当我们涉及到这个的时候,我会对这段代码进行在此优化,并解释有它作为默认参数有什么优势。
queue 队列
简介
所谓队列就是符合某些特征的顺序表亦或者是链表,但是顺序表你要说实现把也能,可是就是顺序表的头插尾删,实在是太耗费时间了,影响效率。所以本人还是推荐用list当作源容器,进行适配器设计。
我们来看一下它的文档,来加深对其的理解
我们不难发现其实和stack一样它也是适配器进行实现的,那么我们也可以依据类似的逻辑进行实现
接口
我们可以看到,队列接口也较为简单,直接进行实现即可
代码实现
template<class T,class container = list<T>>
class queue
{
container _queue_;
public:
void push(const T& val)
{
_queue_.push_back(val);
}
void pop(const T& val)
{
_queue_.pop_front(val);
}
T& front()
{
return _queue_.front();
}
T& back()
{
return _queue_.back();
}
size_t size()
{
return _queue_.size();
}
void swap(queue& q_swap)
{
std::swap(_queue_,q_swap._queue);
}
bool empty()
{
return _queue_.empty();
}
};
dequeue
简介特性
对于这个容器可能你听过它的名字但是没咋,用过他,是的,因为它有一点点挫,所以很少c++程序员使用它,为什么呢?为什么能头尾双端操作的双端队列遭人如此嫌弃??下面让我们一块来了解一下它。
我们从文档中得出它是一个支持双端操作的一个特殊结构,不再是像队列,和栈一样的单方向操作的东西,而只是
接口
我们可以看到几乎vector和list的核心接口都涵盖在,这里边了,这接口数量不可谓不多,其实它一开始出来就是为了对标vector 和list的但是这也导致这个容器啥都会,但是啥都没做到纯粹,就是,效率并不是最高的,贪多嚼不烂,说的就是这个容器,为什么这么说呢,因为这个容器固然头插、头删、尾插、尾删方便。但是它的随机访问和空间占用不比那两个容器。
也就是说它无法做到list,随机删除和插入那麽便利,也无法做到vector的随机访问的效率,导致它处于很是尴尬的局面。而且便利的话,受限于结构,要在多个缓存区中转换,大大降低了效率。,遍历也慢了好多。
当然我们通过这些接口也能窥见一些它的神奇之处,就是我们逻辑上的双端队列,支持的居然是随机迭代器,能够做到迭代器的随机访问,让人无法想象如何遍历。让人叹服。
作为适配器的优势
说起它作为适配器的优点这就不得不说,它的逻辑结构
因为这个容器是一个双端队列,它两个端点都能够进行元素插入,这样的逻辑结构让头尾操作及其简单便利,所以如果用它作为栈和队列的适配器,那么将十分合适,因为这两个数据结构并不涉及,遍历,随机访问,以及随机写入等对于双端队列效率较为低下的操作,所以,上面的缺省值可以写成 deque()
缺点以及底层解释
之所以,我上文如此批判双端队列这主要是因为它的物理结构所决定的,因为它要实现两端的元素操作,这就让他有了一个中控数组,然后如同一个架子一样吊着多个数组
看起来其实挺像vector<vector<int>>的,但是又不是,因为vv存储的是一个有一个对象第一层,然后每个对象又是能自动扩容的vector,但是这个双端队列可就不一样了,他因为要实现随机访问,所以这些数组的大小都是固定的,并不能参差,因为他要针对下标进行映射,查找对应的元素。跳转到不同的内存块中进行访问。
当队列的头部或尾部插入元素时,如果第一个或最后一个数组块已满,会分配一个新的数组块,并将指针指向新的数组块,并且在新的数组中倒序压入数值,这样可以实现在O(1)的时间内存入。相反,当队列的头部或尾部删除元素时,如果第一个或最后一个数组块中的元素变得很少,可能会释放相应的数组块,同时移动指针。
而且在随机访问读写的时候效率十分低下,因为要经历转换,各种 %、\ ,一般过程为,先在最开始的那个数组进行寻找,如果没有减去第一个数组再 / bffer_size 得到在第几个buf ,然后再 % size看是在buf的具体那个位置。
不仅如此,在遍历过程中的迭代器也体现了这一复杂的过程:
在进行遍历的时候如果,cur位于last之前就正常递增,如果位于last,那就将node置于下个节点,让cur跳到node对应的下一个空间。过程之复杂,可见一斑。
而且,因为它开的是一整块数组会影响高速缓冲器的运行,效率不如list。
所以综上,这个容器效率低下是由它的底层结构所决定的。
所以说对于这个容器,两面看待吧,在看到全能性的时候也要注意到,对应的效率问题。
后记
以上是个人理解,如有不妥,谢谢指正,感谢: