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

STL_stack和queue(deque priority_queue)

前言

本文主要介绍,本人的学习心得和知识汇总,本篇博文对于STL知识的讲解侧重于难点,不会每一个都细细讲解。本文主要对适配器设计模式展开讲解,对反向迭代器和优先级队列重点讲解。STL对栈和队列的设计不同于之前c语言设计的栈和队列,不再是手搓结构,我们STL借用别人的容器实现,这就是适配器,要注意适配器是一种设计模式,是一种全新的东西,这也是我们要学习他的原因。

正文

适配器简介

适配器是一种设计模式,就是复用别人的容器,STL里的stack没有自己在实现一个顺序表,而是增加一个类模板,我们一般称这个类模板为Container,stack里的函数再去调用这个类模板的函数。假如你传入了一个vector容器,那么stack里的push既可以去调用vector的push_back。这种设计思想,也叫设计模式,我们称为适配器。大家先这么简单理解,在下面会逐渐地加深理解适配器。

实现适配器的stack和queue (STL)

首先STL的stack和queue和数据结构使用的接口和方法没有什么区别,不过有一个缺点就是STL里的栈和队列里的pop接口是没有返回值,这有点不利于使用,很多时候我们是需要再pop之后接受这个被pop的值的。但是STL却没有实现,其他语言,很多都实现了,隔壁的java就很优秀,实现了返回值。

首先看看栈的代码

namespace star
{template<class T,class Container = deque<T>>class stack{public:void push(const T& x){_con.push_back(x);}void pop(){_con.pop_back();}T& top(){return _con.back();}size_t size(){return _con.size();}bool empty(){return _con.empty();}private://自定义类型Container _con;};
}

是不是很简单,一目了然,这就是适配器的好处,增加了灵活性的同时还简化的代码的编写

这里就是Container就是我们说的外部容器,外部不提供,我们使用默认的vector<T>

template<class T,class Container = vector<T>>

我们再来看一下接口

        void push(const T& x){_con.push_back(x);}void pop(){_con.pop_back();}T& top(){return _con.back();}size_t size(){return _con.size();}bool empty(){return _con.empty();

这些接口,内部就是去调用容器的接口,就像套娃一样,不像c语言,需要自己去实现底层代码,这里增加了复用性,避免了大量的重复性代码块。拿push举例,内部就是去调用容器的push_back()。这样的设计好处就是,我们就是套了个模板,使用者想使用那个容器就上传那个容器,同样使用者也要知道那个容器适合栈结构,这些都由使用者去负责。灵活性和实用性非常高。

我们再来看看队列的代码

namespace star
{template<class T, class Container = deque<T>>class queue{public:void push(const T& x){_con.push_back(x);}void pop(){//_con.erase(_con.begin());_con.pop_front();}T& front(){return _con.front();}T& back(){return _con.back();}size_t size(){return _con.size();}bool empty(){return _con.empty();}private://自定义类型Container _con;};
}

是不是发现和上面的栈差不多,一样就是要个模板,里面复用容器的接口。这里多了一个back()接口,这是STL的一个特别之处,这是因为在实际中,取尾的需求比较频繁,所以,STL就是实现了这个接口。

看到这里那你会发现,很简单是不是,这栈和队列不是so easy吗?c语言干嘛设计这么复杂?其实c语言和c++相比就是,c语言是底层语言,手动挡用的语言。c++就是自动挡的语言,不需要你了解离合与刹车之间的关系,就可以把车开走。但是,你如果不学c语言,底层逻辑。你会发现你的c++代码出了问题,你只能问al,不能自己解决。如果你是这样的一个程序员,那你觉得雇佣你和雇佣al有啥区别?

好了,回到正题,上难度。上面两段代码都有个deque,这是个啥?这其实是一个双端队列?这里为啥要用这个队列呢?stack用vector,queue用list不香吗?我们现在就来学习学习这个deque,看下STL为啥要用这个容器?

来做下题目吧

150. 逆波兰表达式求值 - 力扣(LeetCode)

这道题目,主要是思想上想不到,这里主要运用stack地先进后出地思想,有需要地友友们可以做一下。

deque

首先看看在这个容器的接口

你会发现这个接口既有头插又有尾插,很全面。STL里的vector头插效率很低,直接不提供头插接口,而list呢,尾插效率又低。除非你设计成带头双向链表,这样就很能打了,但是还是避免不了他的空间开销大和随机访问的效率低。那我们能不能实现一个容器,既有vector的优点和list的优点呢?答案就是deque,他就是一个御萝双修的一个产品,但他真的是一个六边形战士吗?我们来讲解下的它实现方式,你就会知道啦。

它的设计是有一个中控器,就是一个指针数组,每个指针指向一段buff数组。他的buff数组大小是固定的,相比vector扩容,他的拷贝数量极少,他拷贝的是指针而已,而不是每个数据,这样效率是极高的,缓解了扩容问题。他的中控器是默认在中间插入,所以他的头插就很方便了,只用看最前面的buff数组是不是满的就行,尾插也是同理。他的这样结构决定了他的随机访问就没有vector极致,他看似连续,实际上不是连续,需要你先算在那个buff,再算在buff的具体位置。可是相比vector,我们就是一个解引用,计算都不用计算。尤其当数据大量的随机访问的时候,他的效率是比较低的相比vector。还有他的中间插入和删除也是一大缺点,首先你就要计算buff位置,其次就是扩容,这里扩容可不能直接增加buff的容量,这样子随机访问的效率会更低,库里也没有这样实现,他的大方向是再开辟一块buff实现。其实你想想,他的中间操作时很复杂的,这里涉及一个中控器,还要控制这个buff数组,首位还好,可是中间的操作,这里变更位置,还要结合迭代器走,他的迭代器是一个自定义类型,里面有四个变量,分别是cur,first,last,node,四个指针,维护遍历。虽然他相比list高速缓存利用率高,可是,中间的插入和删除比list差远了。我们这里简单了解一下就行,这个容器底层实现是很复杂的。这个容器,适合首位的大量插入和删除,但是不适合中间的大量插入和删除。但是它刚刚好适合stack和queue这两个结构。push和pop很适合deque的头尾插入删除的高效率。因此,STL里把deque当作栈和队列的默认容器。其实你会发现,deque的综合性是还可以的,只不过在极致方面上和vector,list差的有点多。

反向迭代器的涉及模式

反向迭代器很好运用了适配器的这种涉及模式,但是相比栈和队列,这里就有点复杂了。因为,这里的反向迭代器,是适用于任何的容器的。

他的底层就是包装了一个正向迭代器,调用正向迭代器的接口实现反向迭代器的功能。我们这里反向迭代器是根据正向迭代器来设计,也就是说,两者的接口都一样。

先给大家看看代码

namespace che
{// 适配器 -- 复用template<class Iterator, class Ref, class Ptr>struct Reverse_iterator{Iterator _it;typedef Reverse_iterator<Iterator, Ref, Ptr> self;Reverse_iterator(Iterator it):_it(it){}//错位设计Ref operator*(){Iterator tmp = _it;return *(--tmp);}//这里复用this指针的operator* 完成错位Ptr operator->(){return &(operator*());}//返回迭代器本身 这里是个适配器self& operator++(){--_it;return *this;}self operator++(int){self tmp(_it);--_it;return tmp;}self& operator--(){++_it;return *this;}self operator--(int){self tmp(_it);++_it;return tmp;}bool operator!=(const self& s) const{return _it != s._it;}bool operator==(const self& s) const{return _it == s._it;}};
}

看一下这里为啥要--
 

	Ref operator*(){Iterator tmp = _it;return *(--tmp);}

看一下这里分析图

这里是对称设计,所以,你再返回的时候需要--获得有效数据,但是这个接口是返回当前迭代器的值,你如果对this指针直接操作,再次访问的时候,值就会改变,这不符合我这个接口的功能需求,所以需要构建一个临时变量,对临时变量操作,这样就不会影响了。

同理这里返回指针也是上面这个原因,这里我们就复用上面的接口就行

        Ptr operator->(){return &(operator*());}

这里需要注意一点,你的反向迭代器是个适配器,不是具体的指针,大家千万不要拿着vector迭代器的思想去往这里套,会绕进去的。这里实际上是一个模板,我们只需要拿一个正向迭代器去往里上传,这个正向迭代器可能是类或者就是一个指针。我们可以只需要提取这些正向迭代器的共性,就是都提供了++,--等重载函数,我们就需要使用这些重载函数完成反向迭代的功能。这里我们在理解底层的时候你不能把它简单理解成一个指针,要把它当成一个模板理解。在使用的时候,你可以把它当成一个指针来理解,因为我们的迭代器设计之初就是效仿指针来是实现的。他的使用语法和指针是一样的,所以,你在使用的时候,把它当成指针来使用完全没理解。在理解底层的时候,你可以一切把他的功能设计是为了实现指针的语法一样去理解他的设计。

下面地赋值重载是重载函数,子打错了


这里的适配器设计模式,很像我们的共厂,只生产标准件。这里的反迭代器就很好的体现了,我不管你的正向迭代器底层咋实现,你只要给我提供这些赋值重载,提供这些接口,让我可以使用就行,管你其他的呢?标准就是这些接口。

优先级队列

优先级队列和我们之前学的队列,实际上底层是翻天覆地的变化。这里的队列更多的是体现的了队列的思想吧。我们暂时把之前数据结构的队列称作普通队列。普通队列满足先进先出,但是优先级队列不满足先进先出,这就是它优先级的体现。他的的数据是按照一定顺序的,比如降升序。但他之所以能叫做队列是因为他和队列相似,都是固定的一端进数据和出数据,就是逻辑结构相似而已。甚至你可以认为他就不是个队列。优先级队列的设计也是采取适配器的设计模式,并且还加入了仿函数。

什么是仿函数

一个类可以向函数一样使用,我们称这种类叫做仿函数,所以仿函数的本质是一个类,一般每部会实现重载函数,从而达到函数的功能。

先看看代码

#pragma once
#include <vector>//优先级队列 不是一个队列 
//底层是一个堆(大小堆)默认是大堆
//这里很奇怪 less<T> 是大堆 greater<T> 是小堆 是反着来的 你可以认为堆就是天生反骨namespace star
{//记得创建Compare对象 你使用的是他的重载方法 这里没有静态 这里不是Java 学java学傻了template<class T,class Container = vector<T>,class Compare = Less<T>>class priority_queue{private://一般建堆的时候用 popvoid AdjustDown(int parent){Compare com;//先左孩子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;}}}//插入数据时用void AdjustUp(int child){Compare com;int parent = (child - 1) / 2;//当child为0的时候就不需要调整了 parent都变成负数了 还向上调整啥while (child > 0){//判断大小if (com(_con[parent], _con[child])){//交换std::swap(_con[child], _con[parent]);//更新child = parent;parent = (child - 1) / 2;}//不需要直接breakelse{break;}}}public:priority_queue(){}template<class InputIterator>priority_queue(InputIterator first, InputIterator last){//插入数据while (first != last){_con.push_back(*first);++first;}//调整数据//向下调整的前提是左右孩子都是一个堆(大堆/小堆)//这里过程类似于递归 for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--){AdjustDown(i);}}//void pop(){std::swap(_con[0], _con[_con.size() - 1]);_con.pop_back();AdjustDown(0);}void push(const T& val){//_con.push_back(val);AdjustUp(_con.size() - 1);}const T& top(){return _con[0];}size_t size(){return _con.size();}bool empty(){return _con.empty();}private:Container _con;};//仿函数/函数对象//仿函数 一个类可以像函数一样使用 替代c语言的函数指针 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;}};
}

很惊讶吧,他竟然有向上调整和向下调整,没错他的底层就是个堆。因此它的容器采取的是vector,而不是list。因为它需要大量的随机访问,并且数组实现结构简单,要知道一个链表实现堆是很复杂的。

他这里的逻辑是,在你实例化对象时,上传一个仿函数,指定优先级,默认是less,也就是大根堆,他这里是反逻辑的,之所以为啥不用greater代表大根堆,我也很疑惑。所以大家记得时候就想,设计堆和二叉数的设计都很反逻辑。那是怎么实现根据你的仿函数而改变大小堆呢?

看一下这里

	void AdjustUp(int child){Compare com;int parent = (child - 1) / 2;//当child为0的时候就不需要调整了 parent都变成负数了 还向上调整啥while (child > 0){//判断大小if (com(_con[parent], _con[child])){//交换std::swap(_con[child], _con[parent]);//更新child = parent;parent = (child - 1) / 2;}//不需要直接breakelse{break;}}}

他这里是直接实例化你的,仿函数的对象,使用对象去比较。这样子我们的代码就不是写死的。而是有点动态的意思,有较高的灵活性。不像以前写的c语言代码,都是死的,不灵活。在这里,你想大堆就大堆,想小堆就小堆,只要传入一个仿函数。

小题目一道

215. 数组中的第K个最大元素 - 力扣(LeetCode)

这里主要运用了大小堆地性质,就是对于排序时的思想,这道题大小堆都可以实现,有需要地友友们也可以做一下。

总结

本篇博客是围绕这适配器展开讲解,大体介绍STL里的栈和队列还有优先级队列,对于双端队列我建议了解即可,学习他其实没什么必要。要是他真的很厉害,我们就不用学vector和list了。这里在学习底层代码地时候,我建议边画图边做,出错了,再调试查看,不建议直接源码,因为一般情况源码也不太好看懂,库里封装地更厉害。

相关文章:

  • HTML网页-练习float
  • Java 注解式限流教程(使用 Redis + AOP)
  • 【C++】C++面向对象设计的核心思想之一: 接口抽象、解耦和可扩展性
  • 【仿生机器人软件架构】通过整合认知系统实现自主精神性——认知系统非常具有可执行性
  • 深入解析 Redis Cluster 架构与实现(二)
  • python打卡day40@浙大疏锦行
  • AWS云创建安全审计用户组
  • 扫地机器人苦寻新引擎,大疆们却已攻入腹地
  • 基于微服务架构的社交学习平台WEB系统的设计与实现
  • 【交通 Traffic Transformer】同一篇文章,内容排版稍有不同 | 交通预测模型中,Transformer相比传统GCN模型有何优势?
  • 基于Vite的前端自动化部署方案
  • AI绘画提示词:解锁创意无限可能
  • 在Linux上安装Docker并配置镜像加速器:从入门到实战
  • C++面试题:Linux系统信号详解
  • 01流程定位
  • 信息化项目验收测试:MES 系统验收测试的测试重点
  • DeepSeek R1 模型小版本升级,DeepSeek-R1-0528都更新了哪些新特性?
  • 【Netty系列】Reactor 模式 1
  • C语言编程--21.两两交换链表中的结点
  • AI赋能SEO关键词策略
  • 建设通是什么网站/seo优化网站词
  • 哈尔滨网站建设兼职/在线生成个人网站免费
  • 个体户可以做企业网站/如何进行品牌营销
  • 利用c 做网站/全国知名网站排名
  • 做电商平台网站/网络营销是干什么的
  • 网站新建需要多久/广告推广赚钱