一文速通stack和queue的理解与使用

ops/2025/1/22 20:52:47/

C++STL之stack和queue

  • 1.stack
    • 1.1.stack的基本概念
    • 1.2.stack的接口
  • 2.queue
    • 2.1.queue的基本概念
    • 2.2.queue的接口
  • 3.priority_queue
    • 3.1.priority_queue的基本概念
    • 3.2.priority_queue的接口
    • 3.3.仿函数
  • 4.容器适配器
  • 5.deque

🌟🌟hello,各位读者大大们你们好呀🌟🌟
🚀🚀系列专栏:【C++的学习】
📝📝本篇内容:stack;stack的基本概念;stack的接口;queue;queue的基本概念;queue的接口;priority_queue;priority_queue的基本概念;priority_queue的接口;仿函数;容器适配器;dequedeque的简单了解;deque的优缺点
⬆⬆⬆⬆上一篇:一文搞懂反向迭代器之C++模拟实现list
💖💖作者简介:轩情吖,请多多指教(> •̀֊•́ ) ̖́-

1.stack

1.1.stack的基本概念

在我们C++的STL中也实现了栈,也就是stack,它的特性是“先进后出”(First In Last Out,FILO)。允许插入和删除的一端称为栈顶,另一端称为栈底。stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为底层的容器,并提供一组特定的成员函数来访问其元素,将特定的类作为底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
在这里插入图片描述
在底层的容器需要有这些成员函数进行操作:empty()、back()、push_back()、pop_back()
我们STL标准库中vector、list都是符合的,但是默认没有为stack指定特定的容器,底层都是使用的deque
在这里插入图片描述

1.2.stack的接口

stack函数说明
stack()构造一个空的栈
size_t size()const;栈有效元素的个数
value_type& top();栈顶的值
void push (const value_type& val);压入一个数据,俗称压栈
void pop();出栈,将数据弹出
bool empty()const;是否为空
#include <iostream>
#include <stack>
using namespace std;
int main()
{//默认构造stack<int> st;//压栈st.push(10);st.push(11);st.push(12);//查看大小cout <<"stack size:" << st.size() << endl;//3//查看栈顶元素cout <<"stack top:"<< st.top() << endl;//12//出栈st.pop();st.pop();st.pop();if (st.empty())//true{cout << "stack is empty" << endl;}return 0;
}

在这里插入图片描述

还可以指定容器,对于deque我们稍后再讲,以及对于它的底层实现有不理解的可以看我下一篇博客

#include <stack>
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{stack<int, list<int>> st1;//底层容器使用的是liststack<int, vector<int>> st2;//底层容器使用的是vectorreturn 0;
}

2.queue

2.1.queue的基本概念

谈到stack就必定会谈到queue,queue就是队列。它的特性是“先进先出”(First In First Out,FIFO),它与stack不同,stack只能在一端操作,但是queue不一样,它的插入操作(进队)只能在表尾,队列的删除操作(出队)只能在表头。能够进行插入元素的一端我们称为队尾,允许删除元素的一端称为队头或队首。
在这里插入图片描述

底层和stack一样,是使用了其他的容器来适配,该容器需要支持这些操作:empty()、size()、front()、back()、push_back()、pop_front()。
我们的list是符合这个条件的(vector没有头插,效率太低),但是底层默认使用的是deque
在这里插入图片描述

2.2.queue的接口

stack函数说明
queue()构造一个空的队列
bool empty()const;判断队列是否为空
size_t size()const;返回队列有效元素的个数
value_type& front();返回队头元素的引用
value_type& back();返回队尾元素的引用
void push (const value_type& val);将元素在队尾入队列
void pop();将队头元素出队列
#include <iostream>
#include <queue>
using namespace std;
int main()
{//默认构造queue<int> q;//入队列q.push(10);q.push(11);q.push(12);q.push(13);//有效元素个数cout << "size:" << q.size() << endl;//4//队头元素cout << "front:" << q.front() << endl;//10//队尾元素cout << "back:" << q.back() << endl;//13//出队列q.pop();q.pop();q.pop();q.pop();//判空if (q.empty())//true{cout << "queue is empty" << endl;}return 0;
}

在这里插入图片描述

3.priority_queue

3.1.priority_queue的基本概念

priority_queue是优先级队列,它也是一种容器适配器,它每一次插入元素,底层就会进行调整,保证它的第一个元素是最大的(最小的)。
它类似于堆,在堆中可以随时插入元素,并且只能检索最大的堆元素(优先级队列中位于顶部的元素)
底层的容器应该是能够支持随机访问迭代器访问的,并且要支持以下操作:empty()、size()、front()、push_back()、pop_back()
在标准容器中,我们的deque和vector都是符合要求的,但是如果没有自己指定容器,那么默认为vector,需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
在这里插入图片描述
我们可以看到其模板参数中还有一个Compare,这其实就是为了使用者能够根据自己的需求,选择大堆还是小堆
我稍微看图来理解一下,具体的实现看我模拟实现的文章,这边主要还是使用为主
在这里插入图片描述
上图假设为vector中所开辟的空间,可能看上去没有什么,那么转换为树型结构呢?如下图
在这里插入图片描述
可以根据公式来看一下:
若i>0,i位置结点的双亲序号:(i-1)/2=parent;i=0,i为根节点编号,无双亲结点
设n为数组的大小,若2i+1<n,左孩子序号:2i+1=leftchild;2i+1>=n否则无左孩子
设n为数组的大小,若2i+2<n,右孩子序号:2i+2=rightchild;2i+2>=n否则无右孩子

可以看到我们的树中,根节点的元素是最小的,其余子树的根节点的元素也是小于孩子节点元素的。
这个事我们的小根堆,大根堆就是反过来,根节点的元素是最大的,其余子树的根节点的元素也是大于孩子节点元素的
我们的priority_queue默认情况下是大堆,其底层是按照小于比较的

3.2.priority_queue的接口

priority_queue函数说明
priority_queue();构造一个空的优先级队列
priority_queue(InputIterator first, InputIterator last);使用迭代器构造
bool empty() const;判断优先级队列是否为空
size_t size()const;优先级队列有效元素的个数
const value_type& top() const;返回优先级队列中最大(最小)的元素,即堆顶的元素
void push (const value_type& val);优先级队列中插入元素
void pop();删除优先级队列中最大(最小)的元素,即堆顶的元素
#include <iostream>
#include <queue>
using namespace std;
int main()
{//默认构造,大堆priority_queue<int> pq;//插入元素pq.push(10);pq.push(1);pq.push(32);pq.push(23);pq.push(16);//priority_queue的有效元素个数cout << "priority_queue size:" << pq.size() << endl;//5//最大元素cout<<"priority_queue top1:"<<pq.top()<<endl;//32//删除堆顶元素pq.pop();//现在堆顶是第二大元素cout << "priority_queue top2:" << pq.top() << endl;//23pq.pop();pq.pop();pq.pop();pq.pop();//叛空if (pq.empty())//true{cout << "priority_queue is empty" << endl;}return 0;
}

在这里插入图片描述

3.3.仿函数

前面我们提到过,我们的优先级队列可以通过传递模板参数改变到底是存储大堆还是小堆,接下来先演示一下
在这里插入图片描述
可以看到我们的优先级队列的模板参数传递时多了一个greater类,它是在functional头文件中的,它其实就是一个普通的类中重载了运算符(),这是用来优先级队列内部改变比较的方法,从而达到改变大小堆的切换,还是那句话,具体的实现看我的模拟实现文章,这边主要讲述使用

优先级队列所存储的元素是自定义类型的时候,那么这个自定义类型需要重载比较运算符,这是因为优先级队列的底层为了保证堆的特性,进行比较调整
下面是演示代码:

#include <iostream>
#include <queue>
#include <functional>
using namespace std;
struct Date
{Date(int year=0,int month=0,int day=0):_year(year),_month(month),_day(day){}//对于自定义的类,需要重载小于<、大于>bool operator>(const Date& d)const{if (_year > d._year){return true;}else if (_year == d._year && _month > d._month){return true;}else if (_year == d._year && _month == d._month && _day > d._day){return true;}return false;}bool operator<(const Date& d)const{if (_year < d._year){return true;}else if (_year == d._year && _month < d._month){return true;}else if (_year == d._year && _month == d._month && _day < d._day){return true;}return false;}int _year;int _month;int _day;};namespace lnb
{//这里便于理解,模拟实现一下greater,和lesstemplate<class T>struct greater{bool operator()(const T& x,const T& y)const//重载运算符(){return x > y;}};template<class T>struct less{bool operator()(const T& x, const T& y)const{return x < y;}};}
int main()
{priority_queue<Date, vector<Date>, lnb::less<Date>> pq;pq.push(Date(2024, 1, 1));pq.push(Date(2024, 2, 5));pq.push(Date(2024, 12, 2));pq.push(Date(2024, 7, 9));pq.push(Date(2024, 1, 4));const Date& d = pq.top();//注意top()的返回值是const属性的cout << "priority_queue top:" <<d._year<<"/"<<d._month<<"/"<<d._day<<endl;// 2024/12/ 2return 0;
}

上面的代码也实现了一下greater和less类,默认情况下,我们的priority_queue用来比较的类是less。
大家把一个类中重载了()运算符的类叫做仿函数,为什么呢?因为它的使用像一个函数,使用类来模拟函数行为的技术,也叫作函数对象

//一个简单的仿函数使用
#include <iostream>
using namespace std;
struct Compare
{bool operator()(const int& x, const int& y)const{return x > y;}
};
int main()
{//bool flag=Compare()(1, 2);//使用匿名对象来直接调用Compare com;bool flag=com(1, 2);.//像函数一样调用if (flag){cout << "大于" << endl;}else{cout << "小于" << endl;}return 0;
}

4.容器适配器

在我们前面的讲述的stack、queue、priority_queue都是容器适配器。
适配器是一种设计模式,该种模式是将一个类的接口转换成客户希望的另外一个接口。
虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,priority_queue默认使用vector
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

deque_367">5.deque

deque_368">5.1.deque的简单了解

之前提及了那么多次的deque,现在该讲它了,它的底层是非常复杂的,我们只需要对它有个简单的了解即可,如果想要学习具体的完整源码可以去看候捷老师的STL源码剖析,如果需要电子版,可以私信我

deque是一种双向开口的连续线性空间,能够在头尾两端分别做元素的插入和删除操作,和vector相比,vector只是单向开口的连续线性空间,虽然从技术观点上来说,vector也可以在首尾两端进行操作,但是它的头部操作需要进行移动数据,效率极差。我们的deque能够在常数时间内对头部进行操作,并且我们的deque没有所谓容量的概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。deque的迭代器也是随机迭代器,能够支持随机访问,但是其底层是经过了封装的,极其复杂。

deque采用一块连续的map作为主控,map是一小块连续的空间,其中的每一个元素都是指针,指向另一段连续线性空间,称为缓冲区。所以说deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组。
在这里插入图片描述
我们来看一下STL源码剖析中的图来更好的理解

// 见 deque_buf_size()。BufSize 默认值为 0 的唯一理由是为了闪避某些
// 编译程序在处理常数算式(constant expressions)时的臭虫。
// 默认使用alloc为配置器
template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
public: // Basic typestypedef T value_type;typedef value_type* pointer;typedef size_t size_type;public: // Iteratorstypedef deque_iterator<T, T&, T*, BufSiz> iterator;protected: // Internal typedefs// 元素的指针的指针(pointer of pointer of T)typedef pointer* map_pointer;protected: // Data membersiterator start;  // 表现第一个节点。iterator finish; // 表现最后一个节点。map_pointer map; // 指向 map,map是块连续空间,// 其每个元素都是个指针,指向一个节点(缓冲区)size_type map_size; // map内有多少指标...
};

上面的代码也是摘取自STL源码剖析,其中我们可以通过图和代码有个大致的了解,map中存储了指向各个缓冲区的指针,通过map我们就可以找到任意一个元素。其中比较重要的就是还有它的迭代器。
我们依旧集合图片和代码来看
在这里插入图片描述

template <class T, class Ref, class Ptr, size_t BufSiz>
struct deque_iterator { // 未继承 std::iteratortypedef deque_iterator<T, T&, T*, BufSiz> iterator;typedef deque_iterator<T, const T&, const T*, BufSiz> const_iterator;static size_t buffer_size(){return deque_buf_size(BufSiz,sizeof(T));}// 未继承 std::iterator,所以必须自行撰写五个必要的迭代器相应型别typedef random_access_iterator_tag iterator_category; // (1)typedef T value_type;                                 // (2)typedef Ptr pointer;                                  // (3)typedef Ref reference;                                // (4)typedef size_t size_type;typedef ptrdiff_t difference_type;                    // (5)typedef T** map_pointer;typedef deque_iterator self;//保持与容器的联结T* cur; // 此迭代器所指之缓冲区中的现行(current)元素T* first; // 此迭代器所指之缓冲区的头T* last; // 此迭代器所指之缓冲区的尾(含备用空间)map_pointer node; // 指向管控中心...
};

我们的迭代器内部的四个成员,能够保证找到任意一个元素。我们可以通过node来进行跳转到上一个或下一个缓冲区,通过first和last来保证因为插入或删除导致越界访问,cur指向当前访问的元素。
在我们deque中,也包含了start和finish两个迭代器,它们分别是begin()和end()的返回值,所以说要注意finish中cur的指向。因为迭代器访问会不停的往前迭代,那么最后的终止条件是遇到最后一个元素的下一个位置,所以说finish中的cur指向并不是最后一个元素。
看一下下面的这张图来理解一下,一个迭代器类型的对象的指向:
在这里插入图片描述

deque_444">5.2.deque的优缺点

优点:
①相比vector来说,扩容的代价比较低,vector扩容需要搬用元素,而deque则只需要增加一段线性空间即可
deque的头插头删、尾插尾删效率高,我们的vector在头插时可能需要扩容并且还要移动元素,因此效率极差
③支持随机访问,我们的list却没法做到
④其底层是连续空间,空间利用率比较高,不需要存储额外字段,这点list也做不到
缺点:
deque的中间插入和删除就不是很好的了,因为这也是有可能需要移动大量的元素
这边有两种情况:每个buff数组不一样大,效率会提高,但是会影响随机访问效率;每个buff数组固定大小,牺牲中间插入删除效率,随机访问效率就变高了。我们的SGI的STL中使用是后者,为了保证随机访问
②最主要的缺点是没有vector和list的优点极致
③但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构

stack和queue选择deque作为底层默认的容器是因为deque完美的避开了缺点,展现出了优点。stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据),内存利用率比list高;queue中的元素增长时,deque不仅效率高;出队时,头删效率也高,比vector强,而且内存使用率高,比list强。

🌸🌸C++STL之stack和queue的知识大概就讲到这里啦,博主后续会继续更新更多C++的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪


http://www.ppmy.cn/ops/152276.html

相关文章

Pytest+Allure+Excel接口自动化测试框架实战

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1. Allure 简介 简介 Allure 框架是一个灵活的、轻量级的、支持多语言的测试报告工具&#xff0c;它不仅以 Web 的方式展示了简介的测试结果&#xff0c;而且允许…

图数据库 | 19、高可用分布式设计(下)

相信大家对分布式系统设计与实现的复杂性已经有了一定的了解&#xff0c;本篇文章对分布式图数据库系统中最复杂的一类系统架构设计进行探索&#xff0c;即水平分布式图数据库系统&#xff08;这个挑战也可以泛化为水平分布式图数据仓库、图湖泊、图中台或任何其他依赖图存储、…

Python新春烟花

目录 系列文章 写在前面 技术需求 完整代码 下载代码 代码分析 1. 程序初始化与显示设置 2. 烟花类 (Firework) 3. 粒子类 (Particle) 4. 痕迹类 (Trail) 5. 烟花更新与显示 6. 主函数 (fire) 7. 游戏循环 8. 总结 注意事项 写在后面 系列文章 序号直达链接爱…

2025年前端面试题汇总

JavaScript核心 异步编程 Promise、async/await 的工作原理及应用场景。如何处理并发请求&#xff0c;使用Promise.all()或Promise.race()等方法。解释事件循环机制&#xff0c;理解微任务&#xff08;microtask&#xff09;与宏任务&#xff08;macrotask&#xff09;的区别。…

工业相机 SDK 二次开发-Halcon 插件

本文介绍了 Halcon 连接相机时插件的使用。通过本套插件可连接海康 的工业相机。 一. 环境配置 1. 拷贝动态库 在 用 户 安 装 MVS 目 录 下 按 照 如 下 路 径 Development\ThirdPartyPlatformAdapter 找到目录为 HalconHDevelop 的文 件夹&#xff0c;根据 Halcon 版本找到对…

14-美妆数据分析

前言 美妆数据分析可以帮助企业更好地理解市场趋势、客户偏好和产品表现 import pandas as pd import numpy as np 一、数据清洗 data pd.read_csv(rC:\Users\B\Desktop\美妆数据.csv,encodinggbk) data.head()data.info()data data.drop_duplicates(inplaceFalse) data.r…

docker运行长期处于activating (start)

当systemctl start docker启动docker卡住长时间无响应&#xff0c;使用systemctl status docker查看docker运行状态发现activating (start) since 二 1998-01-06 00:43:48 CST; 38min ago,这个状态表示启动中&#xff0c;还未启动完成active (running),可以尝试以下操作&#x…

蓝桥杯 单词重排

问题描述 解题思路 这个问题可以通过计算排列数来解决。由于字符串 "LANQIAO" 由7个不同的字母组成&#xff0c;我们可以使用排列公式 P(n,n)n! 来计算&#xff0c;其中 n 是字母的数量。但是&#xff0c;由于字符串中存在重复的字母&#xff0c;我们需要对重复的字…