当前位置: 首页 > news >正文

【C++】:stack、queue和deque全面讲解

希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!       

目录

1.stack

1.1.stack的介绍

1.2.stack的使用

1.3.stack模拟实现

2.queue

2.1.queue的介绍

2.2.queue的使用

2.3.queue模拟实现

3.deque

3.1.deque介绍

3.2.deque的结构

3.3.deque的迭代器

3.4.deque作为默认底层容器的原因


        本篇博客将介绍 stack、queue和deque的使用,以及stack和queue的模拟实现问题。

1.stack

1.1.stack的介绍

  stack核心要点

工作方式:是 后进后出的(LIFO)的容器适配器,最后放入的元素最先取出。

底层容器: 默认使用deque<T>作为底层容器存储数据;也可以使用其他的容器,例如vector、list,但是这些容器要支持empty(判空)、size(取大小)、back(访问末尾元素)、push_back(末尾添加)、pop_back(末尾删除)这些操作。(使用前提

操作特点:元素从栈顶(即对应容器的末尾)进行压入(push)和弹出(pop)。

综上:stack是STL(标准模板库中的)中容器适配器,他提供了一种先进后出的数据结构;没有自己的数据存储,而是去封装一个已有的底层序列容器来实现功能。

        容器适配器简单的说类似于电源适配器,电源适配器可以将交流电转换成手机、平板、电脑需要的直流的电。而stack容器适配器将底层的数据结构转换成一个栈的形式的数据结构。

stack存在的必要性:

  1. stack作为容器适配器可以将支持适配器接口的底层容器转换为栈,这样就可以使用vector或者deque、list的为底层容器的栈。避免vector的模拟stack导致的繁琐出错。
  2.  提高可读性,明确的意图,stack的就是一个LIFO结构,比采用vector模拟stack调用vector的接口更加的清晰。
  3. 将底层实现细节进行封装,避免了对底层数据结构的管理,更加的省事。

stack的缺点

  • 访问方式:只能访问栈顶元素(O(1)),不支持随机访问;
  • 插入/删除:只能在栈顶插入或者删除;
  • 迭代器:不支持迭代器。
  • 底层实现:可以是list、vector和deque(默认)。

       栈的工作方式,从底层存储上看和vector的结构类似,不过是删除了几个功能,也就是上述的缺点了:不可以随机访问,不可以在任意的位置插入。

        综上,

  • vector像一个开放书架,可以随时的取放或者在任何一层放入书籍。
  • stack像是一个羽毛球筒,只能在开口处放球或者取球(最先放进去的先拿出来)。

1.2.stack的使用

        stack的使用非常的直观,其核心接口就是为了LIFO设计的。

函数说明接口说明
stack()构造空的栈
empty()检测stack是否为空
size()返回stack‘中元素的个数
top()返回栈顶元素的引用
push()在栈顶压入元素
pop()将栈顶元素弹出

比较简单,下面是一小段的测试程序

void test_stack()
{stack<int> st;//插入元素st.push(1);st.push(2);st.push(3);st.push(4);st.push(5);st.push(6);//栈中元素的数量cout << st.size() << endl;while (!st.empty())//判空{cout << st.top() << " ";//栈顶元素st.pop();   //出栈}
}

     因为stack采用的时是其他的容器,因此其操作的时间复杂度取决于底层的容器

底层容器push 复杂度pop 复杂度备注
vectorO (1)O(1)扩容时为 O (n),但摊销后是常数时间
dequeO(1)O(1)两端操作都为常数时间
listO(1)O(1)不需要移动元素,只需调整指针

  这里留下一个问题,为什么stack的底层默认容器时deque,不是vector和list呢?

1.3.stack模拟实现

        根据上述所描述的栈的特点和功能,可以把stack认为是阉割版的vector,当然,list也是可以的。

        这里我把模拟实现的stack需要调用到的函数,在C++网站中找出(vector为例)。在stack的模拟实现中就是对vector等容器的接口复用,对底层容器进行封装,输出用户需要的数据结构。这样做既减少了重复代码,又保持了数据结构的特

namespace XLZ
{template <class T, class Container = deque<T>>class stack{public:void pop(){_c.pop_back();}void push(const T& val){_c.push_back(val);}const T& top()const{return _c.back();}size_t size() const{return _c.size();}bool empty() const {return _c.empty();}private:Container _c;//采用vector作为底层的容器};void test_stack(){//stack<int> st;// 用 vector 作为底层容器//stack<int, vector<int>> st;// 用 list 作为底层容器stack<int, list<int>> st;//插入元素st.push(1);st.push(2);st.push(3);st.push(4);st.push(5);st.push(6);//栈中元素的数量cout << st.size() << endl;while (!st.empty())//判空{cout << st.top() << " ";//栈顶元素st.pop();   //出栈}}
}

Container容器,用来存储数据的对象,它管理着一组元素的内存空间,并提供访问和操作这些元素的方法。

        Container = 数据结构 + 操作方法

        容器分为顺序容器(顺序存储元素,vector、list)、关联容器(按 键(key)存储和访问元素

set、map )、容器适配器(stack、queue)三类。

Container con;

Container :容器类型(模板参数),例如vector<int>、list<int>

con: 变量名 。定义一个名为 con 的 Container 类型的对象。Container为模板参数,占位符作用,等待具体类型替换。

2.queue

2.1.queue的介绍

核心要点:

工作方式:是一种容器适配器遵循先进先出(FIFO)的原则,元素从容器的一端插入,从另一端提取

底层容器: 默认使用deque<T>作为底层容器存储数据;也可以使用其他的容器,例如vector、list,但是这些底层容器要支持empty(判空)、size(取大小)、back(访问末尾元素)、push_back(后端插入元素)、pop_front(前端插入元素)、front(访问后端元素)这些操作。(使用前提

操作特点:它通过封装特定的容器类对象作为底层容器来实现,提供一组特定的成员函数来访问元素,元素从底层容器的 “后端” 推入,从 “前端” 弹出。。

综上:deque是STL(标准模板库中的)中容器适配器,他提供了一种先进先出的数据结构;没有自己的数据存储,而是去封装一个已有的底层序列容器来实现功能,仅仅对外暴露符合FIFO的受限的接口。

queue存在的必要性:

  1. 同stack一样,避免其他底层容器的模拟实现栈的时候出现错误,减少bug可以在同一个类模板下实现链队列和顺序表队列避免程序的繁琐
  2. 明确代码意图,同stack一样,让其他开发者明确代码的处理顺序。
  3. 避免效率的浪费,假如采用栈来模拟实现队列,那么头插和头删的效率都是O(n),而采用deque是时间复杂度为O(1).

queue的缺点

  • 访问方式:只能访问队头或者队尾元素(O(1)),不支持随机访问
  • 插入/删除:只能在队尾插入元素,在从队头移除元素不支持中间插入
  • 迭代器:不支持迭代器。设计遵从FIFO不需要对队列中的元素进行迭代。因此也不支持直接的遍历元素,若要遍历,可以借助底层的容器的迭代器,超出queue自身的能力。
  • 底层实现:可以是list、vector和deque(默认),会继承底层容器的局限性,例如vector低下的头删(pop_front)。

综上,

  • list像一个像一个双向链表的书架,你可以在任意位置插入或取下一本书,也可以从左到右或从右到左浏览所有书。
  • queue像是一个单向通行的传送带,只能在起始位置放入,在传送带的尾端拿去,不能放在中间或拿去中间货物,因为是禁止的。

2.2.queue的使用

        queue接口设计极简,核心围绕 “FIFO 的插、删、查”,需结合具体场景掌握用法。

接口功能描述注意事项
push(val)在队尾插入元素val调用底层容器的push_back()
pop()删除队头元素无返回值,需先判空(否则行为未定义)
front()返回队头元素的引用仅能访问队头,需先判空
back()返回队头元素的引用仅能访问队尾,需先判空
empty()判断队列是否为空判空是back()/pop()/front()的前置操作
size()返回队列中元素的个数无迭代器,无法遍历,需通过size()/empty()判断循环次数

一段测试的程序,也可以用于模拟实现程序的测试

void test_queue()
{queue<int> q;q.push(1);q.push(2);q.push(3);q.push(4);q.push(5);cout << q.size() << endl;cout << q.back() << endl;//访问队尾元素while (!q.empty()){cout<< q.front()<<" ";q.pop();}
}

        在上述的提到过容器适配器没有迭代器,无法遍历队列中的元素,那么我需要查看队列中的元素如何做。“遍历”(traversal)指的是在不修改原容器的前提下,按顺序访问所有元素

  1. 将队列元素转移到支持遍历的数据结构中
  2. 使用底层容器
  3. 使用size()函数循环front再pop实现的遍历。(破坏性遍历垃圾,不可取)

  这里也留下一个问题,为什么stack的底层默认容器时deque,不是vector和list呢?list的头插和头删都是O(1),为什么不用list作为底层容器呢?

2.3.queue模拟实现

    根据上述所描述的栈的特点和功能,可以把stack认为是阉割版的list。

        这里我也把模拟实现的queue需要调用到的函数,在C++网站中找出(listr为例)。

namespace XLZ
{template <class T,class Container = deque<T>>class queue{public:bool empty(){return _con.empty();}T& front(){return _con.front();}T& back(){return _con.back();}void pop(){_con.pop_front();}void push(const T& val){_con.push_back(val);}size_t size(){return _con.size();}private:Container _con;};void test_queue(){queue<int> q;q.push(1);q.push(2);q.push(3);q.push(4);q.push(5);cout << q.size() << endl;cout << q.back() << endl;//访问队尾元素while (!q.empty()){cout << q.front() << " ";q.pop();}}
}

        没有什么难点,不做多余的赘述。容器模板参数给缺省参数的情况将会在下一篇博客中讲述;这里只要知道可以这么用就可以了。

3.deque

3.1.deque介绍

        对于deque我们了解即可,使用较少。它是一个底层容器。

deque(双端队列),是标准库中的一种序列容器,其也是一个动态数组。大小是动态变化的,且两端(前端和后端)都能高效地插入/删除元素,时间复杂度为O(1)

vector的对比

相似点:都能随机访问元素(像数组那样通过下标快速取元素),接口也很像,能做类似的事。

不同点:

  •  增长方式(扩容逻辑不一样):vector依靠单个数组存储元素,数组满了就重新分配一个更大的数组,复制元素,耗时间;deque把元素存在多个小块中,内部记录这些信息,所以超长序列下,deque的增长更高效(不用频繁的大规模复制)。
  • 存储方式:和vector是本质的区别,deque的内存不是一块单一的连续空间,而是由多个不连续的、固定大小的内存块组成,通过一个 中控数组(map) 来管理这些内存块的指针。因此deque是不能通过指针偏移的方式进行乱访问的。所以其遍历的

缺点

  1. deque不适合在序列的中间插入和删除元素,deque的效率不如list/forward_list(前向链表)。
  2. 中间元素的改变会导致所有的迭代器失效,迭代器不稳当

 综上   deque 是“两端都灵活” 的动态容器,适合首尾操作多、序列很长的场景;但中间操作多的话,选list类容器更好。

上述是对C++官网上的翻译的介绍和解读。

3.2.deque的结构

        deque的内存结构更像是一个二维数组。

1.分段连续存储的块(buffer数组)

deque的元素被拆分为多个固定大小的连续数组(块,每个块的大小由实现定义(通常足够大以减少内存分配开销)。这些块在物理内存中不必连续,但通过逻辑上的 “串联” 模拟成一个连续序列。

2.中央指针数组

        deque使用二级指针数组(通常成为map),管理所有块的首地址,其中每个元素都是指针指向某一块的起始地址。通过map,分散的块被 “虚拟串联”,使得外部可以通过索引快速定位到任意块。

  • 当需要在首位扩展空间时,deque会新分配一个块,并将其指针插入到 map的头部(前端扩展)和尾部(后端扩展;
  • 当map本身的容量不足的时候;(即存储块指针的数组满了),deque会重新分配一个更大的map数组将旧指针拷贝过去,再释放旧空间。

        假设为前端插入,需要分配一个新的块(数组),并将此数组的首个元素的地址存储到中控数组中,要从中间开始向前放置数组指针;数组中存放从后向前放

       假设为后端插入,需要分配一个新的块(数组),并将此数组的首个元素的地址存储到中控数组中,要从中间开始向后放置数组指针;数组中存放从前向后放

3.3.deque的迭代器

deque 的迭代器需要处理 “跨块遍历”,因此比普通迭代器更复杂。

迭代器的结构,其迭代器和list一样需要专门的类封装。

 T* cur;       // 指向当前元素T* first;     // 指向当前块的起始地址T* last;      // 指向当前块的末尾地址(最后一个元素的下一个位置)T** node;     // 指向 map 中当前块的指针(二级指针)

cur:定位当前元素

first/last:标记当前块的边界,判断迭代器是否需要 “跨块”;

node:关联到map中对应块的指针,实现块之间的跳转。

deque中的首尾迭代器

start:指向第一个块的第一个元素;

finish:指向最后一个块的最后一个元素的下一个位置

这两个迭代器标记了 deque 的整体范围,结合map可快速定位首尾的块。即begin()和end()。

综上:根据其结构总结一下deque的优势和代价。

优势

  • 两端操作高效O(1):只需在map首尾添加 / 删除块指针,无需移动大量元素;
  • 随机访问接近 O(1):通过 map定位块,再在块内偏移,时间复杂度接近vector
  • 内存利用更灵活:分段分配减少了大数组重新分配的开销。

代价

  • 迭代器复杂需处理跨块跳转,实现成本高影响了数据的随机访问和遍历以及operator[ ]的效率。  
  • 中间插入 / 删除仍较慢:需移动块内元素,且可能触发map扩容map就是中控数组。

        deque 是用 “多个小连续数组 + 指针数组管理” 的方式,在 “两端操作效率” 和 “随机访问效率” 之间做了平衡。

图解

//迭代器遍历
auto it = dq.begin();
while (it != dq.end())
{cout << *it << " ";it++;
}

3.4.deque作为默认底层容器的原因

        我们根据stack以及queue的特性,不需要遍历只需要在固定一端或者两端进行操作,而deque头插和头删操作丝毫不逊于list的效率,而且deque的空间利用率高于list。

        相比于vector ,虽然deque的访问效率可能由于空间不连续导致的效率较低;但是对于头插和头删deque的效率较高。

综上:deque虽然访问上的效率低于vector,中间插入和删除数据的效率低于deque,但是无法确认时deque是适合stack和queue的底层数据结构。

http://www.dtcms.com/a/406298.html

相关文章:

  • 【MySQL学习笔记】数据库的CURD(一)
  • 使用Excel在标签打印软件快速新建标签,表格导入并实现批量打印
  • WEB日常刷题练习(1)
  • 信阳网站建设的费用潍坊网站建设维护
  • Kafka-消息不丢失
  • 如何查看一个网站用什么程序做的wordpress文章发布保存都不行
  • ReactFlow:构建交互式节点流程图的完全指南
  • 实战:基于 BRPC+Etcd 打造轻量级 RPC 服务——从注册到调用的核心架构与基础实现
  • 多语言网站建设幻境网站开发人员的岗位有
  • 19.9咖啡项目:工程项目级别的IIC主从机模块
  • 【遥感技术】​从CNN到Transformer:基于PyTorch的遥感影像、无人机影像的地物分类、目标检测、语义分割和点云分类
  • PyTorch深度学习遥感影像地物分类与目标检测、分割及遥感影像问题深度学习优化技术
  • html5如何实现网站开发俄文网站推广
  • Vue3》》 ref 获取子组件实例 原理
  • 【C++实战㊶】C++建造者模式:复杂对象构建的秘密武器
  • stm32h743iit6 USB FS 启用 VBUS 或 BCD 前后的区别
  • 资源网站模板网页qq登陆手机版网址
  • vue中.env文件是什么,在vue2和vue3中的区别
  • ADMM 算法的基本概念
  • Vue中如何封装双向绑定的组件
  • 个人网站建设与维护上传wordpress到空间
  • 深入剖析Spring Boot依赖注入顺序:从原理到实战
  • 对象关系映射(ORM)
  • 在VS Code 中为Roo Code 添加 Bright Data 的本地MCP服务器
  • 专业的制作网站开发公司wordpress界面404
  • Python Pillow库详解:图像处理的瑞士军刀
  • AI 时代的安全防线:国产大模型的数据风险与治理路径
  • Deepoc具身智能模型:为传统机器人注入“灵魂”,重塑建筑施工现场安全新范式
  • 鸿蒙NEXT安全控件解析:实现精准权限管控的新范式
  • 创建自己的网站广告图片