Stack和Queue的详解

💻前言

关于 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++程序员使用它,为什么呢?为什么能头尾双端操作的双端队列遭人如此嫌弃??下面让我们一块来了解一下它。

我们从文档中得出它是一个支持双端操作的一个特殊结构,不再是像队列,和栈一样的单方向操作的东西,而只是

接口

我们可以看到几乎vectorlist的核心接口都涵盖在,这里边了,这接口数量不可谓不多,其实它一开始出来就是为了对标vector 和list的但是这也导致这个容器啥都会,但是啥都没做到纯粹,就是,效率并不是最高的,贪多嚼不烂,说的就是这个容器,为什么这么说呢,因为这个容器固然头插、头删、尾插、尾删方便。但是它的随机访问和空间占用不比那两个容器。

也就是说它无法做到list,随机删除和插入那麽便利,也无法做到vector的随机访问的效率,导致它处于很是尴尬的局面。而且便利的话,受限于结构,要在多个缓存区中转换,大大降低了效率。,遍历也慢了好多。

当然我们通过这些接口也能窥见一些它的神奇之处,就是我们逻辑上的双端队列,支持的居然是随机迭代器,能够做到迭代器的随机访问,让人无法想象如何遍历。让人叹服。

作为适配器的优势

说起它作为适配器的优点这就不得不说,它的逻辑结构

因为这个容器是一个双端队列,它两个端点都能够进行元素插入,这样的逻辑结构让头尾操作及其简单便利,所以如果用它作为栈和队列的适配器,那么将十分合适,因为这两个数据结构并不涉及,遍历,随机访问,以及随机写入等对于双端队列效率较为低下的操作,所以,上面的缺省值可以写成 deque()

缺点以及底层解释

之所以,我上文如此批判双端队列这主要是因为它的物理结构所决定的,因为它要实现两端的元素操作,这就让他有了一个中控数组,然后如同一个架子一样吊着多个数组

看起来其实挺像vector<vector<int>>的,但是又不是,因为vv存储的是一个有一个对象第一层,然后每个对象又是能自动扩容的vector,但是这个双端队列可就不一样了,他因为要实现随机访问,所以这些数组的大小都是固定的,并不能参差,因为他要针对下标进行映射,查找对应的元素。跳转到不同的内存块中进行访问。

当队列的头部或尾部插入元素时,如果第一个或最后一个数组块已满,会分配一个新的数组块,并将指针指向新的数组块,并且在新的数组中倒序压入数值,这样可以实现在O(1)的时间内存入。相反,当队列的头部或尾部删除元素时,如果第一个或最后一个数组块中的元素变得很少,可能会释放相应的数组块,同时移动指针。

而且在随机访问读写的时候效率十分低下,因为要经历转换,各种 %、\ ,一般过程为,先在最开始的那个数组进行寻找,如果没有减去第一个数组再 / bffer_size 得到在第几个buf ,然后再 % size看是在buf的具体那个位置。

不仅如此,在遍历过程中的迭代器也体现了这一复杂的过程:

在进行遍历的时候如果,cur位于last之前就正常递增,如果位于last,那就将node置于下个节点,让cur跳到node对应的下一个空间。过程之复杂,可见一斑。

而且,因为它开的是一整块数组会影响高速缓冲器的运行,效率不如list。

所以综上,这个容器效率低下是由它的底层结构所决定的。

所以说对于这个容器,两面看待吧,在看到全能性的时候也要注意到,对应的效率问题。

后记

以上是个人理解,如有不妥,谢谢指正,感谢:

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

难扰浮生梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值