stl_stack/queue

一.适配器

stackqueue实际上并不能算是一种容器,而是一种容器适配器。而适配器作为stl的6大组件之一,其实是一种设计模式。适配器模式其实就是将一个类的接口(该接口无法直接满足客户的需求)转换成客户希望的另一个接口,以满足客户的需求。

而对于stack和queue来说,其就是借助另一个容器实现了LIFO(last in firtst out)和FIFO(first in first out)的功能。

而我们看到stack和queue的第二个模板参数容器给了缺省值,如果不指定容器,就会默认用deque来实现queue和stack。当然也可以利用其他的容器来实现,但是容器的结构一定得符合queue和stack的压入和弹出规则。

 二.模拟实现stack

模拟实现stack时,必须要遵循stack的规则LIFO(last in first out)。而栈的接口只有压栈、出栈、取栈顶元素等操作,而我们用vector就可以完美实现stack的接口。

压栈直接调用vector的push_back即可,出栈调用vector的pop_back,取栈顶元素调用vector的back。

namespace xsc
{template<typename T,typename Container = vector<T>>class stack{public:void push(const T& val){_con.push_back(val);}void pop(){_con.pop_back();}T& top(){return _con.back();}bool empty(){return _con.empty();}int size(){return _con.size();}private:Container _con;};
}

但是当我们了解了list的结构之后,发现其也可以很好的实现stack的各种接口。所以我们使用栈时,既可以用缺省参数vector,也可以显式给出容器list。

xsc::stack<int> is;
xsc::stack<double, list<double>> is2;

三.模拟实现queue

模拟实现queue时,我们要了解其规则FIFO(first in first out).当我们要借助其他容器来实现queue时就不能再用vector来实现了。因为queue涉及到了删除队头的数据,vector并没有头删的接口,当然list和vector都有erase的接口,我们可以借助这个来实现,但是用vector会涉及到数据的挪动影响效率。所以我们默认采取list来实现queue。

queue的push借助list的push_back,pop借助list的pop_front,back、front分别借助list的back和front 

namespace xsc
{template<typename T,typename Container = list<T>>class queue{public:void push(const T& val){_con.push_back(val);}void pop(){_con.pop_front();}T& front(){return _con.front();}T& back(){return _con.back();}bool empty(){return _con.empty();}int size(){return _con.size();}private:Container _con;};
}

四.deque

虽然我们借助list或者vector实现了stack和deque,但是我们看到标准库中栈和队列的容器参数默认是deque。那什么是deque呢?

deque实际是是一个双端队列,也是一个顺序容器,是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端 进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与 list比较,空间利用率比较高。

我们可以看一下deque都实现了那些接口:

 deque其实是vector和list的结合体。

因为vector和list都有其各自的优点和一些不足,所以设计容器的人就想是否可以设计出一个容器既包含vector的优点也包含list的优点——所有deque就问世了。

但是deque并没有达到预期,如果deque真的实现了vector和list优点的结合,那么vector和list就不会再用人用了。

1.vector和list的对比

 vector的优点:

        1、尾插尾删效率不错,支持高效下标随机访问

        2、物理空间连续,所以高速缓存利用率高

vector的缺点:

        1、空间需要扩容,扩容有一些代价(效率和空间浪费),且这个代价到后期数据多的时候更大

        2、头部和中间插入/删除效率低(挪动数据)

 list的优点:

        1、按需申请释放空间,不需要扩容

        2、支持任意位置O(1)的插入删除

list的缺点:

        1、不支持下标的随机访问

2.deque的结构 

deque的底层结构并不是一段连续的物理空间,而是类似于一个二维数组。由一个中控数组和若干个buff组成。

中控数组map其实是一个指针数组,其中存储着指向一个个buffer的指针。而buffer是一个定长数组(或者动态数组),所有的数据都存储在buffer上。

不同的是,中控数组map是从中间开始存储的,第一个buffer的指针存储在中控数组的中间部分,如果要头插就在该位置的前面存储一个buffer的地址。当map满了之后还需要对map进行扩容。

deque还重载了[] ,那么deque是如何支持下标访问的呢?

 我们可以采取/和%的方式,定位到指定下标的元素位置。

假设要获取下标为i的元素,每一个buffer的大小为N。我们先用 x = i / N就可以得到该元素位于第几个buffer中;然后用y = i % N就可以知道该元素是这个buffer的第几个位置,然后借助两次operator[][]的调用map[x][y]就可以得到该数据。

3.deque的迭代器 

deque的迭代器是由4个指针来实现的

cur指向当前正在访问的元素,first指向这段buffer的开始位置,last指向这段buffer的结束位置,node指向中控数组中存储这个buffer的位置。

借助这个迭代器,deque模拟出来物理空间连续的假象。 

当我们遍历时,当cur等于last时说明这个buffer已经遍历完了,此时需要走到下一个buffer,只需要让node++即可。

4.deque的维护

deque这个容器的维护主要是借助两个迭代器来实现的:

这两个迭代器就相当于begin()和end()。分别指向开始和结尾。 

 遍历容器时,其实是创建一个迭代器,然后将start的值给他,然后通过cur来遍历。

5.deque的push_back()/push_front()

deque的尾插和头插效率都很高,尾插直接向buffer里面插入即可,如果buffer满了在申请一个即可。

deque的头插数据的方式有些许不同。头插时是往中控数组的的前面插入一个buffer,然后将头插的元素插入到buffer的尾部。

要注意的是头插和尾插之后start和finish都需要改变。 

 6.deque的insert/erase

当在中间位置插入删除时,有两种方式:

1、挪动数据,保证buffer的大小不变,但是这样就会导致效率极低

2、扩大或缩小buffer,但是这样会导致下标访问会更加困难,不能用/和%的方式得出下标位置。

总结:

1、deque的头尾插入删除效率很高,更甚于vector和list

2、下标随机访问也还不错,但略逊于vector

3、中间位置的插入删除效率很低O(N),需要挪动数据

 7.deque的缺陷

与vector比较,deque的优势是:头部插入和删除时,不需要挪动数据,效率特别高,而且在扩容时,也不需要搬移大量数据,因此其效率是比vector高的。

与list比较,其底层空间是连续空间,空间利用率比较高,不需要存储额外字段

但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器需要频繁的去检查其是否移动到buffer的边界,导致效率低下,而在序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构

8.为什么选择deque作为stack和queue的底层默认容器

stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list;

queue是先进先出的特殊线性数据结构,只要具有push_back()和pop_front()操作的线性结构,都可以作为queue的底层容器,比如:list。

但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:

        1、stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作

        2、在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率还高。

结合了deque的优点,而完美的避开了其缺陷。

9.对比deque和vector的排序效率 

我们从结果可以分析出,在存储相同数据的情况下,不论是直接比较deque和vector,还是用两个存储相同数据的deque,一个直接排序,一个先将数据拷贝到vector中,排序,拷贝会去,效率都是采用vector的高。

这是因为在sort算法下采用的是快排,快排需要对数据进行访问,而vector对数据的随机访问效率要比deque高得多。

void test_op1()
{srand(time(0));const int N = 1000000;deque<int> dq;vector<int> v;for (int i = 0; i < N; ++i){auto e = rand() + i;v.push_back(e);dq.push_back(e);}int begin1 = clock();sort(v.begin(), v.end());int end1 = clock();int begin2 = clock();sort(dq.begin(), dq.end());int end2 = clock();printf("vector:%d\n", end1 - begin1);printf("deque:%d\n", end2 - begin2);
}void test_op2()
{srand(time(0));const int N = 1000000;deque<int> dq1;deque<int> dq2;for (int i = 0; i < N; ++i){auto e = rand() + i;dq1.push_back(e);dq2.push_back(e);}int begin1 = clock();sort(dq1.begin(), dq1.end());int end1 = clock();int begin2 = clock();// 拷贝到vectorvector<int> v(dq2.begin(), dq2.end());sort(v.begin(), v.end());dq2.assign(v.begin(), v.end());int end2 = clock();printf("deque sort:%d\n", end1 - begin1);printf("deque copy vector sort, copy back deque:%d\n", end2 - begin2);
}

五.priority_queue 

1.priority_queue基本概念

priority_queue就是优先级队列,它也是一种容器适配器。但是它不同于queue,不再以deque作为底层容器,而是vector。

 而priority_queue在底层是vector的前提下,又采用了堆的算法,将其包装成一个堆。所以priority_queue其实就是一个堆(默认是大堆)。其设计的一系列接口也都是模拟了堆的行为。

top取出堆顶的元素(最大/最小的元素)

push插入数据然后采用向上调整算法使其依旧是一个堆 

pop删除堆顶的数据然后采用向下调整算法使其依旧是一个堆

在使用向上(向下)调整算法时,要保证其原本就是堆。


小伙饿坏了,赶紧点了一份爱学的二叉树——堆,狼吞虎咽学完很过瘾-CSDN博客

在这里可以回顾一下堆的基本操作:

1、堆是一个完全二叉树,完全二叉树就是前面每层都是满的,最后一层从左到右是连续的。 

2、知道一个父亲节点的下标parent,怎么求它的左右孩子:left = 2*parent+1,right  = 2*parent+2

3、知道孩子节点的下标child,怎么求父亲节点:parent = (child-1)/2.

4、大堆:父亲节点的值大于等于左右孩子的值,但左右孩子之间没有大小关系

5、小堆:父亲节点的值小于等于左右孩子的值,但左右孩子之前没有大小关系。

6.插入节点:插入节点是插入到最后一层从左到右的最后一个位置,但是插入必须要保证插入之后也是一个堆,所以插入之后要进行向上调整,与它的父亲节点比较,如果是大堆的话,插入的大于父亲节点就交换,直到小于某个父亲节点或者到达堆顶就停止交换。

7.删除堆顶元素:首先将堆顶元素与最后一个节点交换,然后再删除最后一个节点,然后进行向下调整,使之仍然是堆


优先级队列之所以默认是大堆,原因在于其第三个模板参数 ,当我们改变第三个模板参数时,就可以将其变为小堆

//priority_queue<int> q; // 大堆
priority_queue<int,vector<int>,greater<int>> q; // 小堆

2.模拟实现priority_queue

默认是大堆

插入之后向上调整时,将孩子与父亲进行比较,如果孩子大于父亲就交换,直到孩子小于父亲/已经交换到了堆顶。

删除之后向下调整时,父亲与左右孩子大的那个交换(采取假设法:先假设左孩子大,再判断右孩子是否大于左孩子,如果大于那么child+1),直到父亲大于左右孩子,或者已经交换到最后一层。

小伙饿坏了,赶紧点了一份爱学的二叉树——堆,狼吞虎咽学完很过瘾-CSDN博客

namespace xsc
{//默认是大堆template <typename T, typename Container = vector<T>>class priority_queue{public:void AdjustUp(int child){int parent = (child - 1) / 2;while (child > 0){if (_con[child] > _con[parent]){swap(_con[child], _con[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}}void push(const T& val){_con.push_back(val);AdjustUp(_con.size() - 1);}void AdjustDown(int parent){int child = parent * 2 + 1;while (child < _con.size()){if (child + 1 < _con.size() && _con[child] < _con[child + 1])//只有这个条件可能会越界:a[child] > a[child + 1]{child += 1;}if (_con[parent] < _con[child]){swap(_con[child], _con[parent]);parent = child;child = parent * 2 + 1;}else{break;}}}void pop(){swap(_con[0], _con[_con.size() - 1]);_con.pop_back();AdjustDown(0);}T& top(){return _con[0];}bool empty(){return _con.empty();}int size(){return _con.size();}private:Container _con;};
}

那如果要实现一个小堆呢?难道要写出两个类模板么?

在标准库中的priority_queue是借助第三个模板参数来实现的:

//priority_queue<int> q; // 大堆
//priority_queue<int, vector<int>, less<int>>;// 第三个默认参数是less<int>priority_queue<int,vector<int>,greater<int>> q; // 小堆

 而这第三个模板参数其实是借助仿函数来实现的。

3.仿函数

仿函数人如其名,它并不是一个函数,而是一个重载了()的一个类或者结构体,它可以使其看上去像一个函数一样,像函数一样使用。仿函数可以接受参数并返回值,可以用于STL算法中的函数对象参数,也可以用于函数指针的替代。

template <typename T>
struct Less
{bool operator()(const T& x, const T& y){return x < y;}
};int main()
{Less<int> l;cout << l(1, 2) << endl;return 0;
}

我们实际上是借助这个类创建了一个对象,但是我们使用这个对象的方式却是调用函数的行为,这就是仿函数。

实际上是调用了该类的operator()。

cout << l.operator(1, 2) << endl;

我们可以实现两个仿函数Less和Greater来封装比较的逻辑,对priority_queue增加第三个模板参数,并将其内部的比较都换成仿函数的比较方式,以此来改变向上向下调整的比较逻辑,实现大堆/小堆。

在库中,less默认是<,gerater默认是>,所以我们采取和库里一样的比较方式。

template <typename T>
struct Less
{bool operator()(const T& x, const T& y){return x < y;}
};template <typename T>
struct Greater
{bool operator()(const T& x, const T& y){return x > y;}
};

 用第三个模板参数创建一个对象,然后用该对象调用operator(),进行比较。注意在比较时要注意符合符号以及是大堆还是小堆。


完!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/59042.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

利用Docker Compose构建微服务架构

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 利用Docker Compose构建微服务架构 引言 Docker Compose 简介 安装 Docker Compose 创建项目结构 编写 Dockerfile 前端 Dockerf…

Redis未授权访问漏洞复现和修复建议

Redis未授权访问漏洞利用&#xff08;总结&#xff09; 一、漏洞介绍及危害1.1 原理1.2 漏洞影响版本1.3 漏洞危害1.4 实战中redis常用命令 二、漏洞复现2.1 环境准备2.1.1 靶机安装redis服务器2.1.2 kali安装Redis客户端&#xff08;Redis-cli&#xff09; 三、漏洞利用3.1 利…

无人机之集群控制方法篇

无人机的集群控制方法涉及多个技术和策略&#xff0c;以确保多架无人机能够协同、高效地执行任务。以下是一些主要的无人机集群控制方法&#xff1a; 一、编队控制方法 领航-跟随法&#xff08;Leader-Follower&#xff09; 通过设定一架无人机作为领航者&#xff08;长机&am…

流水线商品标签如何快速打印?商品标签自定义打印软件操作方法

一、概述 【软件可定制详情点文章最后信息卡片】 流水线商品标签如何快速打印&#xff1f;商品标签自定义打印软件操作方法 ‌定义与用途‌ 商品标签打印软件&#xff0c;即用于打印商品标签的应用软件。标签包含产品上的文字、商品详情等说明信息 如图&#xff0c;可以预先…

不只是任务分配!管理者应具备的核心认知

背景 二十年&#xff0c;中国的互联网行业飞速发展&#xff0c;让无数年轻人有了从技术岗走向管理岗的机会。然而&#xff0c;许多工程师在走上管理岗位时往往是“仓促上任”&#xff0c;没有足够时间适应管理工作和责任。少数悟性高、能力突出的工程师能够迅速胜任&#xff0…

第二十五章 Vue父子通信之sync修饰符

目录 一、概述 二、完整代码 2.1. main.js 2.2. App.vue 2.3. BaseDialog.vue 三、运行效果 一、概述 前面的章节我们讲到&#xff0c;通过v-model我们可以实现父子组件间的通信&#xff0c;但是使用v-model的时候&#xff0c;子组件接收的prop属性名必须固定为valu…

【浪潮商城-注册安全分析报告-无验证方式导致安全隐患】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

轻松成为文本文件管理大师,将每个文件夹中的所有TXT分别合并成一个文本文档,发现办公软件TXT合并功能的无限可能

文本文件如潮水般涌来&#xff0c;管理它们成为了一项令人头疼的任务。但是&#xff0c;别怕&#xff0c;有了首助编辑高手软件&#xff0c;你将成为办公软件达人&#xff0c;轻松驾驭这些文本文件&#xff0c;体验无限魅力&#xff01;想象一下&#xff0c;杂乱无章的文件夹瞬…

安卓13默认连接wifi热点 android13默认连接wifi

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.编译6.彩蛋1.前言 有时候我们需要让固件里面内置好,相关的wifi的ssid和密码,让固件起来就可以连接wifi,不用在手动操作。 2.问题分析 这个功能,使用普通的安卓代码就可以实现了。 3.代…

青春的海洋:海滨学院班级回忆录项目

3系统分析 3.1可行性分析 通过对本海滨学院班级回忆录实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本海滨学院班级回忆录采用SSM框架&#xff0c;JAVA作为开…

基于matlab的线性卷积演示系统

文章目录 前言1. 卷积的简单介绍1.1 翻褶1.2 移位1.3 相乘1.4相加1.5 整体的运行效果展示 2.App Designer的介绍3.具体的开发步骤3.1 声明成员变量3.2 设计基本布局3.3 编写回调函数 4.运行展示结语 前言 本篇文章按照如下要求&#xff0c;完成线性卷积演示系统 (1)用matlab完…

如何在Linux命令行中使用GhatGPT

2、验明正身&#xff0c;证明我的所在地是国内 3、第一次提问 4、第二次提问 5、问他一首古诗 6、话不多说&#xff0c;现在来展示他的安装过程 7、输入GitHub的网址 https://github.com/aandrew-me/tgpt 8、详情页向下翻 9、到终端输入 下列命令&#xff0c;等待安装&#x…

java并发编程-volatile的作用

文章目录 volatile的作用1.改变线程间的变量可见性2.禁止指令重排序 参考的学习视频 volatile的作用 1.改变线程间的变量可见性 每个线程都有一个专用的工作集内存&#xff0c;下图里面粉色的表示专用工作集内存&#xff0c;黄色的是共享内存工作区&#xff0c;如果加入了vol…

linux中级(防火墙firewalld)

一。firewalld与iptables区别1.firewalld可以动态修改单条规则&#xff0c;不需要像iptables那样&#xff0c;修改规则后必须全部刷新才可生效。firewalld默认动作是拒绝&#xff0c;则每个服务都需要去设置才能放行&#xff0c;而iptables里默认是每个服务是允许&#xff0c;需…

C#/.NET/.NET Core学习路线集合,学习不迷路!

前言 C#、.NET、.NET Core、WPF、WinForm、Unity等相关技术的学习、工作路线集合&#xff08;持续更新&#xff09;&#xff01;&#xff01;&#xff01; 全面的C#/.NET/.NET Core学习、工作、面试指南&#xff1a;https://github.com/YSGStudyHards/DotNetGuide C#/.NET/.N…

Linux 实例:无法通过 SSH 方式登录

现象描述 使用 SSH 登录 Linux 实例 时&#xff0c;提示无法连接或者连接失败&#xff0c;导致无法正常登录 Linux 实例。 现象描述 处理措施 SSH 登录报错 User root not allowed because not listed in AllowUsers 排查 SSH 登录报错 User root not allowed because not …

后端Java学习:springboot之文件上传(阿里云OSS存储)

一、什么是阿里云存储&#xff1f; 阿里云对象存储OSS&#xff08;Object Storage Service&#xff09;&#xff0c;是一款海量、安全、低成本、高可靠的云存储服务。使用OSS&#xff0c;您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。 二、阿里云…

当下的力量:拥抱自我与持续学习的旅程

许多人被无尽的选择与信息所淹没&#xff0c;常常感到迷茫与焦虑。然而&#xff0c;真正的力量来自于对当下的专注&#xff0c;以及对自我的深刻理解与不断提升。如何在喧嚣中找到内心的宁静&#xff1f;如何在复杂的世界中坚持学习与成长&#xff1f;这不仅是一个时代的问题&a…

【android12】【AHandler】【3.AHandler原理篇AHandler类方法全解】

AHandler系列 【android12】【AHandler】【1.AHandler异步无回复消息原理篇】-CSDN博客 【android12】【AHandler】【2.AHandler异步回复消息原理篇】-CSDN博客 其他系列 本人系列文章-CSDN博客 1.简介 前面两篇我们主要介绍了有回复和无回复的消息的使用方法和源码解析&a…

GPRS是什么?

‌GPRS&#xff08;General Packet Radio Service&#xff09;‌是一种基于GSM&#xff08;Global System for Mobile Communications&#xff09;系统的无线分组交换技术&#xff0c;提供端到端的广域无线IP连接。与传统的GSM系统不同&#xff0c;GPRS采用分组交换技术&#x…