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

devtools/2025/1/22 12:43:02/

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/devtools/152598.html

相关文章

【Vim Masterclass 笔记18】第八章 + S08L35:Vim 的可视化模式(二)

文章目录 S08L35 Visual Mode - Part 21 利用可视化模式控制代码块的缩进2 缩进宽度的设置3 仅对选中区域执行替换操作4 利用可视化模式实现文本对齐 写在前面 本篇为 Vim 可视化模式的第二部分&#xff0c;主要介绍了可视化模式在代码缩进方面的应用。该视频应该录制于 2018 年…

UE5 开启“Python Remote Execution“

demo 代码 remote_execution.py 远程调用UE5 python代码-CSDN博客 在启用 Unreal Engine 5&#xff08;UE5&#xff09;的“Python 远程执行”功能后&#xff0c;UE5 会启动一个 UDP 组播套接字服务&#xff0c;以监听来自外部应用程序的 Python 命令。 具体行为如下&#xf…

Arm 计划涨价高达 300%,并考虑自行研发芯片

Arm 计划涨价高达 300% 据财联社 1 月 14 日消息&#xff0c;芯片技术供应商 Arm Holdings&#xff08;Arm&#xff09;正在制定一项长期战略&#xff0c;计划将其芯片设计授权费用提高高达 300%&#xff0c;并考虑自主研发芯片&#xff0c;以与其最大的客户展开竞争。以下是详…

ChatGPT大模型极简应用开发-目录

引言 要理解 ChatGPT&#xff0c;了解其背后的 Transformer 架构和 GPT 技术一路的演进则变得非常必要。 ChatGPT 背后的 LLM 技术使普通人能够通过自然语言完成过去只能由程序员通过编程语言实现的任务&#xff0c;这是一场巨大的变革。然而&#xff0c;人类通常容易高估技术…

亲测有效!如何快速实现 PostgreSQL 数据迁移到 时序数据库TDengine

小T导读&#xff1a;本篇文章是“2024&#xff0c;我想和 TDengine 谈谈”征文活动的优秀投稿之一&#xff0c;作者从数据库运维的角度出发&#xff0c;分享了利用 TDengine Cloud 提供的迁移工具&#xff0c;从 PostgreSQL 数据库到 TDengine 进行数据迁移的完整实践过程。文章…

ZooKeeper 中的 ZAB 一致性协议与 Zookeeper 设计目的、使用场景、相关概念(数据模型、myid、事务 ID、版本、监听器、ACL、角色)

参考Zookeeper 介绍——设计目的、使用场景、相关概念&#xff08;数据模型、myid、事务 ID、版本、监听器、ACL、角色&#xff09; ZooKeeper 设计目的、特性、使用场景 ZooKeeper 的四个设计目标ZooKeeper 可以保证如下分布式一致性特性ZooKeeper 是一个典型的分布式数据一致…

html转义符+h5提供的新标签

html转义符 h5提供的新标签 HTML5是HTML从传统的web端开始兼容移动互联网的重要标志&#xff0c;h5为HTML提供了大量好用的标签&#xff0c;如布局使用的三个标签header、section、footer标签&#xff1b;用来播放视频和音频的多媒体标签video、audio标签等&#xff0c;参考表…

Vue+Element-ui 中 使用el-table 设置表格单元格 (el -table-column) 保留空格和换行

遇到的问题 在使用 el-table 展示数据时&#xff0c;单元格中的数据中存在多个空格和换行符&#xff0c;若不进行设置&#xff0c;浏览器默认会取消空格和换行符。 解决办法 将单元格的样式 “white-space” 属性设置为“pre-wrap” 即可解决 ::v-deep .el-table .cell {wh…