C++:stack queue - 容器适配器

C++:容器适配器

    • 容器适配器概念
    • stack
    • queue
    • deque


容器适配器概念

容器适配器是在C++标准库中提供的一种容器的封装。它们提供了一种统一的接口,使得不同类型的容器可以以相似的方式被使用。容器适配器有三种类型:栈(stack)、队列(queue)和优先队列(priority_queue)。其中优先队列其实就是数据结构中的堆(heap)。

我们看到这三种数据结构有一个共同的特点,那就是它们的规则是基于数据的,而不是基于内存的。比如说顺序表(vector)要求内存必须是连续的,链表(list)要求一个一个节点的形式来存储数据。它们都对内存有明确的限制。而以上三种数据结构,只是对数据的出入顺序有要求,所以它们的底层可以是vectorlist等等其它容器。

也就是说,它们可以将原本的容器进行封装,改变容器的出入规则,但是底层的数据存储依然使用其它的容器。这种模式叫做容器适配器。

接下来带大家实现stackqueue,帮助大家理解这种容器适配器模式。


stack

栈(stack)是一种先进后出(Last-In-First-Out,LIFO)的数据结构,它的特点是只能在栈的一端进行插入和删除操作。在栈中,插入元素的一端称为栈顶(top),删除元素的一端称为栈底(bottom)。栈的插入操作叫做入栈(push),删除操作叫做出栈(pop)。

栈的实现通常有两种方式:数组实现和链表实现。使用数组实现的栈叫做顺序栈,使用链表实现的栈叫做链式栈。
在此处,我们用vector作为底层容器,实现一个顺序栈。
在这里插入图片描述
以上就是STL库中stack的所有接口,我们选择里面最重点实现。

先看到基本结构:

template <class T>
class stack
{
private:
	vector<T> _con;
};

stack类内部,有一个成员变量_con,其类型为vector<T>,这就是我们stack的底层容器,后续我们stack进行操作,本质上都是对vector进行操作。

那么我们的vector哪一边做栈顶好呢?
如果我们用vector的头部做栈顶,那么每次入栈,所有数据都要往后移动一位;每次出栈,所有数据都要往前移动一位。
这会带来大量的时间浪费。
但是用vector的尾部做栈顶,那么每次入栈出栈,都不会影响其它数据,所以最好用尾部做栈顶。

入栈:
入栈其实就是对vector尾插:

void push(const T& x)
{
	_con.push_back(x);
}

出栈:
出栈就是对vector尾删:

void pop()
{
	_con.pop_back();
}

返回栈顶元素:
返回栈顶,其实就是得到vector尾部的元素:

const T& top()
{
	return _con.back();
}

返回栈的大小:
栈的大小,就是vector的大小:

size_t size()
{
	return _con.size();
}

判断栈是否为空:
栈为空,其实就是vector为空:

bool empty()
{
	return _con.empty();
}

可以发现,我们将底层设为vector后,无需再考虑底层是如何插入删除,只需要将我们配套的规则对应的接口用上去即可,这就是容器适配器带来的优势。
但是至此,还不算完整的容器适配器设计模式。如果用户想用list做底层,难道我们又要写一个list版本吗?
这是不需要的,我们不如直接传入第二个模板参数,让用户可以自定义需要的底层容器:

template <class T, class Container = vector<T>>
class stack
{
private:
	Container _con;
};

第二个模板参数container就是我们的底层容器,其默认值为vector,当用户不传入第二个参数,那么默认以vector为底层容器。

当用户需要list版本的stack

stack<int, list<int>> st;

这样我们就可以让用户想用什么底层容器,就用什么底层容器。

完整代码:

template <class T, class Container = deque<T>>
class stack
{
public:
	void push(const T& x)
	{
		_con.push_back(x);
	}

	void pop()
	{
		_con.pop_back();
	}

	const T& top()
	{
		return _con.back();
	}

	size_t size()
	{
		return _con.size();
	}

	bool empty()
	{
		return _con.empty();
	}

private:
	Container _con;
};

queue

队列(Queue)是一种先进先出(First-In-First-Out,FIFO)的数据结构。在队列中,元素只能在一端(队尾)添加,而在另一端(队头)删除。新的元素只能添加到队尾,而只能从队头删除元素。
在这里插入图片描述
同样的我们来实现一下queue:

相比于vectorlist更适合作为queue的底层容器,因为queue需要头部删除,而vector头部删除的代价很大,要移动整个vector的数据,而list不需要,所以queue适合用list作为底层。

有了前面的铺垫,这里我就不额外讲解每个接口了:

template <class T, class Container = list<T>>
class queue
{
public:
	void push(const T& x)
	{
		_con.push_back(x);
	}

	void pop()
	{
		_con.pop_front();
	}

	const T& front()
	{
		return _con.front();
	}

	const T& back()
	{
		return _con.back();
	}

	size_t size()
	{
		return _con.size();
	}

	bool empty()
	{
		return _con.empty();
	}

private:
	Container _con;
};

前面的两个容器适配器,一个用list做底层,一个用vector做底层。但是在STL中,这两个容器适配器的默认底层容器都是一个叫做deque的容器,接下来我为大家介绍这个容器。


deque

deque是双端队列(double-ended queue)的缩写,它是一种具有特殊特性的线性数据结构。deque允许从两端进行元素的插入和删除操作。

我们看一下deque的结构:
在这里插入图片描述
deque分为两个区域,左下角的区域称为中控,其用于控制所有小数组,中控本质上是一个指针数组,内部的每个指针都指向了一个小数组。而我们是在右侧小数组中插入删除数据的。

deque的插入并不是从第一个位置开始插入,而是从中间开始插入
比如我们要插入数据“12345”
在这里插入图片描述
这么做的好处就是:适合头插尾插。
接下来我们再在头部插入数据“678910”

在这里插入图片描述

可以看到,我们进行头部插入,不会影响其他数据,这是它相对于vector的优势。而由于其内存是部分连续的,可以通过中控数组的指针偏移量与小数组的指针偏移量来锁定元素。所以其也可以支持下标随机访问。

deque的优势:

  1. 头尾插入删除效率很高
  2. 支持下标随机访问

那么它这么好用,为什么不直接替代vector和list呢?
那么我再讲一讲它的致命缺点:极度不适合中间插入
我们看到,这个结构中,数据被分为了很多个小段,如果我们在中间插入删除数据,就会导致数据的挪动很麻烦,因为会发生不同数组之间的数据迁移。所以其中间插入的效率非常非常低。
这也就是为什么它称为双端队列的原因,就是只适合两端的插入删除,

stackqueue两种数据结构,刚好都是不会发生中间的插入删除的,所以它们的默认底层容器是deque

最后总结一下:
deque的特性包括:

  1. 双端高效插入和删除操作:deque允许在队列的头部和尾部进行插入和删除操作。这意味着可以在队列的任意一端进行元素的添加和移除,而不仅限于一侧。插入和删除的时间复杂度是O(1),即常数时间。这使得deque在需要高效地在两端进行插入和删除操作的场景下非常有用。

  2. 随机访问:和数组类似,deque也支持随机访问。即通过下标访问第i个元素的时间复杂度是O(1)。这是因为在数组实现中,deque使用了连续的存储空间,并且通过指针可以直接定位到指定的元素。

  3. 空间效率:deque的空间效率较高。在数组实现中,由于使用了连续的存储空间,没有额外的指针和链表结构,因此空间占用较小。

总结起来,deque是一种具有特殊特性的线性数据结构,它兼具了队列和栈的特点,并且在两端插入和删除操作非常高效,同时也支持随机访问。这使得deque在需要频繁在两端进行插入和删除操作的场景中非常有用。但是不适合中间的插入删除。


容器适配器使得程序员可以不必直接处理底层容器的细节,而是使用统一的接口来操作不同类型的容器。这样,当需要改变底层容器时,只需要修改适配器的类型参数即可,而不需要修改大量的代码。容器适配器的使用可以提高代码的可维护性和可扩展性,同时也使代码更易于理解。


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

人形机器人专题:准直驱执行器深度:人形机器人执行器技术的前沿

今天分享的是人形机器人系列深度研究报告&#xff1a;《人形机器人专题&#xff1a;准直驱执行器深度&#xff1a;人形机器人执行器技术的前沿》。 &#xff08;报告出品方&#xff1a;招商证券&#xff09; 报告共计&#xff1a;34页 准直驱执行器具备独特优势 刚性执行器…

Netty Review - 服务端channel注册流程源码解析

文章目录 PreNetty主从Reactor线程模型服务端channel注册流程源码解读入口 serverBootstrap.bind(port)执行队列中的任务 &#xff1a; AbstractUnsafe#register0注册 doRegister() 源码流程图 Pre Netty Review - ServerBootstrap源码解析 Netty Review - NioServerSocketCh…

【机器学习案例4】为机器学习算法编码分类数据【含源码】

目录 编码分类数据 序数编码 标签编码 一次性编码 目标编码 目标编码的优点 目标编码的缺点 在现实生活中,收集的原始数据很少采用我们可以直接用于机器学习模型的格式,即数值型数据。因此,需要进行一些预处理,以便以正确的格式呈现数据、选择信息丰富的数据或降低其…

C++ 特殊类的实现

一、请设计一个类&#xff0c;不能被拷贝 拷贝只会放生在两个场景中&#xff1a;拷贝构造函数以及赋值运算符重载&#xff0c;因此想要让一个类禁止拷贝&#xff0c;只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。 在C98中&#xff1a;将拷贝构造函数与赋值运算符重载…

2024-2-14-复习作业

1> 要求&#xff1a; 源代码&#xff1a; #include<stdio.h> #define N 50 int main(int argc, char const *argv[]) {int arr[N][N];int n;printf("please enter n :");scanf("%d",&n);for(int i1;i<n;i){for(int j1;j<i;j){if(j1 |…

机器学习---规则学习(序贯覆盖、单条规则学习、剪枝优化)

1. 序贯覆盖 回归&#xff1a; 分类&#xff1a; 聚类&#xff1a; 逻辑规则&#xff1a; 读作&#xff1a;若&#xff08;文字1且文字2且...&#xff09;&#xff0c;则目标概念成立 规则集&#xff1a;充分性与必要性&#xff1b;冲突消解&#xff1a;顺序规则、缺省规则…

vuex中mutations详解,与actions的区别

Vuex 的 Mutations 是用于改变 Vuex Store 中状态的一种方式。它是一个同步的操作&#xff0c;用于直接修改 Store 中的状态。 Mutations 有以下特点&#xff1a; 同步操作&#xff1a;Mutations 是同步的&#xff0c;这意味着它们会立即执行并修改状态。原子性&#xff1a;…

计算机组成原理:存储系统【二】

&#x1f308;个人主页&#xff1a;godspeed_lucip &#x1f525; 系列专栏&#xff1a;计算机组成与原理基础 &#x1f6f0;️1 Cache概述&#x1f6e9;️1.1 局部性原理&#x1f6eb;1.1.1 空间局部性&#x1f6eb;1.1.2 时间局部性 &#x1f6e9;️1.2 性能指标&#x1f6eb…

linux系统zabbix自动发现主机

自动发现主机 新的主机浏览器配置创建发现规则创建发现主机后动作 新的主机 rpm -Uvh https://repo.zabbix.com/zabbix/5.0/rhel/7/x86_64/zabbix-release-5.0-1.el7.noarch.rpm# yum clean allyum install zabbix-agentvim /etc/zabbix/zabbix_agentd.conf Server10.12.153.1…

SpringBoot+Tess4J实现本地与远程图片的文字识别

Spring Boot应用程序里集成Tess4J来实现OCR&#xff08;光学字符识别&#xff09;&#xff0c;以识别出本地和远程图片中的文字 一、添加依赖 <dependency><groupId>net.sourceforge.tess4j</groupId><artifactId>tess4j</artifactId><vers…

Linux信号集与信号集相关函数

阻塞信号集和未决信号集&#xff1a; 例如&#xff1a;当进程收到SIGINT信号后&#xff0c;首先被保留在未决信号集中&#xff0c;此时标识位为1&#xff0c;当这个信号被处理之前&#xff0c;先检查阻塞信号集中对应的编号的位上的标识是否为1&#xff1b; 为1表示该信号被当…

ChatGPT高效提问—prompt实践(法律助手)

ChatGPT高效提问—prompt实践&#xff08;法律助手&#xff09; ​ 作为现代法治国家的公民&#xff0c;无论我们是否从事法律相关的工作&#xff0c;都难免会遇到法律问题&#xff0c;那么如何争取自身合法利益最大化呢&#xff1f;很多人大概率会第一时间查询相关的法律知识…

Java奇缘:林浩然与杨凌芸的数学冒险记

Java奇缘&#xff1a;林浩然与杨凌芸的数学冒险记 Java Adventure: The Mathematical Odyssey of Lin Haoran and Yang Lingyun 在Java编程世界的某一个角落&#xff0c;住着两位才华横溢的程序员——林浩然和杨凌芸。林浩然&#xff0c;人称“算法大侠”&#xff0c;对Java Ma…

C语言中整数除法的特性

目录 介绍 解决方法 例1 例2 介绍 在 C 语言中&#xff0c;整数除法有一个特性&#xff0c;即它会对结果进行截断而不是四舍五入。这意味着无论结果是正数还是负数&#xff0c;除法的结果都将向零取整。这也就是说&#xff0c;C 语言中的整数除法会直接截断小数部分&#x…

Spring Boot 笔记 021 项目部署

1.1 引入坐标&#xff0c;并双击package打包成jar包 1.2 在服务器上运行jar包 1.3 使用postman测试 2.1 运行配置 2.1.1 命令更改端口 java -jar big-event-1.0-SNAPSHOT.jar --server.port7777 2.1.2 环境变量更新&#xff08;略&#xff09; 2.1.3 外部配置文件&#xff0c…

LeetCode:118.杨辉三角

118. 杨辉三角 - 力扣&#xff08;LeetCode&#xff09;&#xff0c; 前言&#xff1a;平平无奇的实现&#xff0c;数组理清了的话就很easy&#xff0c;值得说的是给定的参数 int* returnSize, int** returnColumnSizes 是什么意思&#xff0c;还得熟悉适应&#xff0c;博主…

深入了解pip和conda:高效Python环境管理的必备指南

pip相关命令: 更新包之前最好更新一下pip&#xff0c;因为更新其他包底层是依赖 pip pip show pippython -m pip install --upgrade pippython更新包&#xff1a; - ​ pip install --upgrade 包 pip install pandas- ​ pip install --upgrade 包名称版本号查看那些包需要更…

【leetcode热题100】交错字符串

给定三个字符串 s1、s2、s3&#xff0c;请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。 两个字符串 s 和 t 交错 的定义与过程如下&#xff0c;其中每个字符串都会被分割成若干 非空 子字符串&#xff1a; s s1 s2 ... snt t1 t2 ... tm|n - m| < 1交错 是 s1 …

CAN通讯协议学习

介绍 它是一种异步通讯&#xff0c;can_high和can_low两条线利用的是电位差传输信号&#xff0c;抗干扰能力强&#xff0c;但是必须要有can控制器如TJA1050&#xff08;我的开发板&#xff09; 当 CAN 节点需要发送数据时&#xff0c;控制器把要发送的二进制编码通过 CAN_Tx 线…

牛客JZ 36二叉搜索树与双向链表

描述 输入一棵二叉搜索树&#xff0c;将该二叉搜索树转换成一个排序的双向链表。如下图所示 数据范围&#xff1a;输入二叉树的节点数 0≤n≤10000≤n≤1000&#xff0c;二叉树中每个节点的值 0≤val≤10000≤val≤1000 要求&#xff1a;空间复杂度O(1)&#xff08;即在原树上…