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

C++入门小馆: STL 之queue和stack

嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的passion。准备好和我一起冲进代码的奇幻宇宙了吗?Let's go!

我的博客:yuanManGan

我的专栏:C++入门小馆 C言雅韵集 数据结构漫游记  闲言碎语小记坊 题山采玉 领略算法真谛

目录

怎么使用队列和栈:

队列和栈的模拟实现:

deque:

priority_queue


先来了解

怎么使用队列和栈:

这些接口看起来已经很熟悉了。

直接来看看使用吧:

使用就很简单了。

队列和栈的模拟实现:

我们先来了解一下适配器:

适配器大家比较熟悉的就是电源适配器,我们的充电器。

queue和stack都是一种容器适配器。

我们用其他类来封装这两个容器。

我们回忆一下stack只需要在一方插入和删除,我们vector和list都能满足要求,所以我们来简单来实现一下。

#pragma once
#include<vector>
#include<list>
#include<deque>namespace refrain
{template<class T, class Container = std::deque<T>>class stack{public:T& top() {return _con.back();}const T& top() const{return _con.back();}void push(const T& x){_con.push_back(x);}void pop(){_con.pop_back();}size_t size() const{return _con.size();}bool empty(){return _con.empty();}private:Container _con;};
}

deque:

这里的缺省值给的是deque双端队列,等会再来讲解。

再来回忆一下队列,它是先进先出,一端入数据一端出数据,我们就拿头部出数据,尾部入数据,简单实现如下:

#include<deque>
#include<iostream>
using namespace std;
namespace refrain
{template<class T, class Container = std:: deque<T>>class queue{public:void push(const T& x){_con.push_back(x);}T& front(){return _con.front();}const T& front() const{return _con.front();}T& back(){return _con.back();}const T& back() const{return _con.back();}void pop(){_con.pop_front();}size_t size() const{return _con.size();}bool empty(){return _con.empty();}private:Container _con;};
}

这里我们有个问题我们的vector不是没有pop_front吗,那我们是不是不能用vector来实现队列呢?的确库里面他就是用的pop_front,库里面不想你使用vector来实现,因为vector头部操作麻烦时间复杂度高,那我们能不能强制实现呢?我们可以使用erase来实现头部删除就可以用vector来实现queue了。

这是库里实现的:

这样就适用了,但我们一般用deque这个更优秀

接下来就简单讲讲deque了:

先来对比一下list和vector的各自的优缺点:

而我们vector的优点就是list的缺点,vector的缺点就是list的优点。

优点:缺点
vector

1.支持下标随机访问,

2.CPU高速缓存命中率高

1.头部或中间位置的插入删除操作效率低下,因为要挪动数据

2.扩容有一定的成本,存在一定的浪费。比如现在容量为100满了扩成200只插入了120空间浪费了80空间。

list

1.容易位置的插入和删除的时间复杂度都是O(1),不需要挪动数据。

2.不存在扩容,按需申请释放,不浪费空间。

1.不支持下标随机访问

2.CPU高速缓存命中率低

这里补充一个知识CPU高速缓存命中率的知识:

cpu想要访问内存的一个数据,要看这个数据是否在缓存,在就缓存命中,不在就不命中;不命中就先加载到缓存,然后再访问缓存。

它加载到缓存不是只加载一个数据而是加载一个范围的数据,因为vector创建的是连续的空间,cpu的高高速缓存就高,而list每次都得先加载到缓存然后再访问缓存。

接下来就来简单了解一下deque的底层实现了。 

我们用四个指针封装deque的迭代器。

我们创建多个buff数组,用first指向buff的第一个位置,last指向最后一个位置。用cur指向当前位置。

再创建一个中控数组map,map里面存储着指向buff数组的指针,也就是二级指针。

这样设计有什么好处呢?

当我们迭代器++时

直接++cur,如果cur是最后一个的时候就移动node代码如下:

库里面是这样实现的: 

逻辑是一样的,但它的细节 处理的更好。

减减也是相同的逻辑,就不实现了。

解引用操作就很简单了

直接解引用cur。

push_back:

如果push_back时buff没有慢,就直接cur++,last++。 

当buff已经满了,我们就得创建一个buff了,让node++,然后cur指向buff的第二个元素,last指向末尾,first指向第一个元素,如图。

再来看看push_front

当buff满了,就创buff,此时我们cur不是指向第一个元素,指向的是最后一个元素,要保持连续性,node--,last,first指向尾和头。

再头插一个:

j就直接cur--就行了。

总结一下:

优点:

         1.头尾部插入删除效率很高

         2.下标随机访问效率还可以,但不如vector。

缺点:

         1.中间位置插入和删除效率一般。

         2.对比vector和list没有那么极致。(不专一)

 适合做stack和queue的适配器

priority_queue

优先级队列也不是队列是堆,堆之前我们用c语言就实现过,我们看看stl库里面怎样实现的。

我们之前实现的堆,大家回忆一下,是不是用到了大量的下标访问 ,我们就可以使用vector来当容器适配器。

当我们插入一个数50的时候,这时候还是一个堆,就不进行操作

而当我们插入35的时候:我们要进行向上调整算法。 

 

那我们回忆一下怎么找父亲节点呢?

parent = (child - 1)/ 2。

我们就写一个向下调整算法

如果c语言实现的时候认真学习的应该没有什么问题。

同样的删除堆顶时我们交换堆顶和最后一个位置,然后尾删,进行向下调整算法

我们找左右孩子怎么找到呢?

左孩子: child = parent * 2 + 1

右孩子:child = parent* 2 + 2

一样实现的轻而易举:

 我们就来实现一下堆吧:

template<class T, class Container = std::vector<T>>
class priority_queue
{
public:void push(const T& x){_con.push_back(x);adjustup(_con.size() - 1);}void pop(){std::swap(_con[0], _con[_con.size() - 1]);_con.pop_back();adjustdown(0);}const T& top() const{return _con[0];}bool empty(){return _con.empty();}//大堆void adjustup(int child){int parent = (child - 1) / 2;while (child > 0){if (_con[child] > _con[parent]){std::swap(_con[child], _con[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}}void adjustdown(int parent){int child = parent * 2 + 1;while (child < _con.size()){if (child + 1 < _con.size() && _con[child + 1] > _con[child]){child++;}if (_con[child] > _con[parent]){std::swap(_con[child], _con[parent]);parent = child;child = parent * 2 + 1;}else{break;}}}
private:Container _con;
};

 

这里的top就不能实现两个版本了,因为栈顶的元素不支持修改。 

这里还缺少一个参数是什么呢,这里可以用仿函数来实现:

仿函数

我们可以通过重载( )括号运算符来实现仿函数:

 

我们可以利用这个特性来让我们选择能创的是大堆还是小堆,但我们这个有点坑,传less创建的是小堆,我们只好按它的来哦。

#pragma once
#include<vector>
#include<iostream>namespace refrain
{template<class T>class less{public:bool operator()(const T& x, const T& y){return x < y;}};template<class T>class greater{public:bool operator()(const T& x, const T& y){return x > y;}};template<class T, class Container = std::vector<T>, class Compare = less<T>>class priority_queue{public:Compare com;void push(const T& x){_con.push_back(x);adjustup(_con.size() - 1);}void pop(){std::swap(_con[0], _con[_con.size() - 1]);_con.pop_back();adjustdown(0);}const T& top() const{return _con[0];}bool empty(){return _con.empty();}//大堆void adjustup(int child){int parent = (child - 1) / 2;while (child > 0){if (com(_con[parent], _con[child])){std::swap(_con[child], _con[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}}void adjustdown(int parent){int child = parent * 2 + 1;while (child < _con.size()){if (child + 1 < _con.size() && com(_con[child], _con[child + 1])){child++;}if (com(_con[parent], _con[child])){std::swap(_con[child], _con[parent]);parent = child;child = parent * 2 + 1;}else{break;}}}private:Container _con;};}

 完了over!

相关文章:

  • JavaScript输出数据的方法
  • 解决qnn htp 后端不支持boolean 数据类型的方法。
  • 如何创建并使用极狐GitLab 部署令牌?
  • shell命令二
  • 迈锐思C1pro插件安装包【附百度网盘链接】
  • 用户行为检测技术解析:从请求头到流量模式的对抗与防御
  • DataStreamAPI实践原理——快速上手
  • 【quantity】2 Unit 结构体(unit.rs)
  • S08-电机功能块讨论
  • 全新升级:BRAV-7601-T003高性能无风扇AI边缘计算系统,助力智能未来!
  • 智能指针之设计模式5
  • Xray-安全评估工具
  • 19.TVS特性与使用注意事项
  • 学习spark-streaming收获
  • 7.14 GitHub命令行工具测试实战:从参数解析到异常处理的全链路测试方案
  • 深入Java JVM常见问题及解决方案
  • web基础+HTTP+HTML+apache
  • 回顾|Apache Cloudberry™ (Incubating) Meetup·2025 杭州站
  • 蓝桥杯Java全攻略:从零到一掌握竞赛与企业开发实战
  • 腾讯 Kuikly 正式开源,了解一下这个基于 Kotlin 的全平台框架
  • 中国黄金协会:一季度我国黄金产量同比增1.49%,黄金消费量同比降5.96%
  • 2025上海车展的三个关键词:辅助驾驶、性价比,AI生态
  • 杭州打造商业航天全产业链,请看《浪尖周报》第22期
  • 首映|《人生开门红》:段子背后都是案子
  • 人民日报:光荣属于每一个挺膺担当的奋斗者
  • 女儿被偷拍后,一个父亲的战斗