【C++】STL---list

STL---list

  • 一、list 的介绍
  • 二、list 的模拟实现
    • 1. list 节点类
    • 2. list 迭代器类
      • (1)前置++
      • (2)后置++
      • (3)前置- -、后置- -
      • (4)!= 和 == 运算符重载
      • (5)* 解引用重载 和 -> 重载
    • 3. list 类
      • (1)迭代器
      • (2)修改相关的接口
        • swap()
        • insert()
        • erase()
        • push_back、push_front、pop_back、pop_front
        • clear()
      • (3)空链表初始化
      • (4)构造函数
      • (5)拷贝构造函数
      • (6)赋值运算符重载
      • (7)析构函数
    • 4. 打印容器的接口
      • (1)打印链表整型的接口
      • (2)打印 list 的接口
      • (3)打印容器的接口

一、list 的介绍

  1. list 是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
  2. list 的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
  3. listforward_list 非常相似:最主要的不同在于 forward_list 是单链表,只能朝前迭代,已让其更简单高效。
  4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
  5. 与其他序列式容器相比,listforward_list 最大的缺陷是不支持任意位置的随机访问,比如:要访问 list 的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list 还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大 list 来说这可能是一个重要的因素)。

二、list 的模拟实现

list 学习时也要学会查看文档:list 文档介绍,在实际中我们熟悉常见的接口就可以,下面我们直接开始模拟实现,在模拟实现中我们实现的是常见的接口,并且会在实现中讲解它们的使用以及注意事项。

首先跟以往不一样的是,list 是一个个节点连接起来的,所以它不是连续的物理空间,这也就意味着,它不用扩容,每次插入的时候只需要申请一个节点,然后连接起来即可;

其次,list 底层的迭代器实现也跟 stringvector 不一样,它们两个的迭代器可以说是原生指针,但是 list 的迭代器是要让节点指向下一个节点,所以底层实现也不一样;例如我们想让迭代器 it,往后迭代,就是 ++it,但是底层的实现却不是真的让节点++,因为它们的空间不是连续的,所以我们要把 list 迭代器封装成一个类。

首先我们先创建一个自己的命名空间,把 list 节点的类,list 迭代器的类,list 类都放进去;

1. list 节点类

list 节点类如下,因为是双向链表,所以应该有一个数据,两个指针;

		namespace Young{// list 节点类template <class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& x = T()):_data(x),_next(nullptr),_prev(nullptr){}};}

2. list 迭代器类

首先我们先定义一个类模板,其参数有三个,分别是类型类型的引用(const 和 非const)类型的指针(const 和 非const)

为什么要定义三个模板参数呢,因为考虑到 const 迭代器const 迭代器和普通迭代器不是同一个类,不能直接在 iterator 前直接加 const,如 const iterator ,这不是 const 迭代器,因为这里的 const 修饰的是迭代器本身,就是迭代器本身不能修改,但是我们期望的是迭代器本身可以被修改,如 it++、++it,只是期望迭代器指向的内容不能被修改,如 *it = 10、it->10

这就类比 const T*T* constconst T*const 是修饰指向的内容不能被修改,而 T* constconst 修饰的是指针本身不能被修改;而我们需要实现的 const 迭代器 是要满足第一种的,所以 list普通迭代器const 迭代器 是两个完全不一样的类,应该写成两个类,但是我们可以通过增加两个模板参数 类型的引用(const 和 非const)类型的指针(const 和 非const) 来复用普通迭代器,具体实现如下:

		// list 迭代器类template <class T,class Ref,class Ptr>struct __list_iterator{typedef list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> self;Node* _node;// 迭代器构造函数__list_iterator(Node* node):_node(node){}}

首先我们先将节点类起别名为 Node,再将自己的类起别名为 self;迭代器本身也是一个指针,只是它内部实现不一样,所以我们需要一个 _node 节点的指针,构造函数实例化一个节点的指针,比如说 list<int>::iterator it = lt.begin();,这里的 it 就会调构造函数,实例化一个 lt.begin() 节点的指针,其实 lt.begin() 就是指向头节点的指针。

接着我们重载一些迭代器常用的运算符:

(1)前置++

就是让迭代器往后迭代,具体的实现就是让节点的指针指向下一个节点:

			// 前置 ++self& operator++(){_node = _node->_next;return *this;}

(2)后置++

跟前置++的区别就是,后置++需要拷贝,返回++以前的迭代器,所以一般都不用后置++;

			// 后置 ++self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}

(3)前置- -、后置- -

前置- -、后置- - 与 ++ 的区别就是, - -返回上一个节点的迭代器;

			// 前置 --self& operator--(){_node = _node->_prev;return *this;}// 后置--self operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}

(4)!= 和 == 运算符重载

!= 运算符重载就是比较它们的节点是否相等;== 运算符就相反;

			// != 运算符重载   iterator it != lt.begin();bool operator!=(const self& s){return s._node != _node;}// == 运算符重载   iterator it == lt.begin();bool operator==(const self& s) {return s._node == _node;}

(5)* 解引用重载 和 -> 重载

解引用重载-> 重载 就是改变迭代器指向内容的两个运算符,所以我们定义的三个模板参数,就在这里起作用了;比如我们实例化的模板参数是 const 迭代器__list_iterator<T, const T&, const T*>,这里的 const T& 就是 Refconst T* 就是 Ptr,这里就可以直接用 Ref (解引用重载)和 Ptr(箭头重载) 作返回值;

如果是 非const 迭代器__list_iterator<T, T&, T*>T& 就是 RefT* 就是 Ptr;所以就可以根据它们的类型返回对应的迭代器类型,就不需要我们自己写两个迭代器的类了。

			// * 解引用重载Ref operator*(){return _node->_data;}// -> 重载Ptr operator->(){return &_node->_data;}

解引用-> 重载的使用:

假设 list 里面存的类型是一个自定义类型,这个自定义类型中有两个成员变量,那么我们在使用 解引用-> 重载的时候,应该访问哪一个呢?这时候就需要我们指定访问了,如下代码:

		struct AA{AA(int a1 = 0, int a2 = 0):_a1(a1), _a2(a2){}int _a1;int _a2;};void test4(){Young::list<AA> lt;lt.push_back(AA(1, 1));lt.push_back(AA(2, 2));lt.push_back(AA(3, 3));Young::list<AA>::iterator it = lt.begin();while (it != lt.end()){// 使用解引用//cout << (*it)._a1<<" "<<(*it)._a2 << endl;//使用 ->cout << it->_a1 << " " << it->_a2 << endl;++it;}cout << endl;}

上面的 cout << it->_a1 << " " << it->_a2 << endl; 调用了->重载,实际上是 cout << it.operator->()->_a1 << " " << it.operator->()->_a2 << endl;,本来应该是有两个 -> 的,即 it->->_a1 但是这样写可读性不好,所以编译器特殊处理,省略了一个 ->

3. list 类

list 类首先将 const 迭代器和非 const 迭代器类型起别名为 const_iteratoriterator ;成员变量有 _head 哨兵位节点和 _size 记录链表的长度,如下:

		// list 类template <class T>class list{public:typedef list_node<T> Node;typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;private:Node* _head;size_t _size;};

(1)迭代器

注意,begin() 是哨兵位的下一个节点,end() 是哨兵位节点。

begin()end() 返回的类型也是一个迭代器,这里 iterator(_head->_next) 是调用迭代器类的构造函数,构造一个节点的指针返回;也可以写成 _head->_next,因为支持隐式类型的转换;

			// 非 const 迭代器iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}// const 迭代器const const_iterator begin() const{return const_iterator(_head->_next);}const const_iterator end() const{return const_iterator(_head);}

(2)修改相关的接口

swap()

交换链表数据,需要借助标准库的 swap 函数实现:

			// 交换链表数据void swap(list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}

insert()

pos 迭代器插入节点;新开一个节点,然后插入指定迭代器的位置,连接好 prevcur 的位置即可;因为 list 的底层结构为带头结点的双向循环链表,因此在 list 中进行插入时是不会导致 list 的迭代器失效的;

			// 插入节点iterator insert(iterator pos, const T& x){Node* newnode = new Node(x);Node* cur = pos._node;Node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return newnode;}

erase()

删除 pos 迭代器位置的节点;在删除时迭代器会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响,所以 erase() 函数执行后,it 所指向的节点已被删除,因此 it 无效,在下一次使用 it 时,必须先给其赋值;

			// 删除节点iterator erase(iterator pos){Node* prev = pos._node->_prev;Node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;pos._node->_next = pos._node->_prev = nullptr;--_size;return next;}

push_back、push_front、pop_back、pop_front

只需要复用 insert()erase() 即可,实现如下:

			// 尾插void push_back(const T& x){insert(end(), x);}// 头插void push_front(const T& x){insert(begin(), x);}// 尾删void pop_back(){erase(--end());}// 头删void pop_front(){erase(begin());}

clear()

清空链表数据,删除除了哨兵位的节点即可;

			// 清空链表数据void clear(){iterator it = begin();while (it != end()){it = erase(it);}}		

以上修改接口配合迭代器的使用如下图:

在这里插入图片描述

在这里插入图片描述

(3)空链表初始化

			// 空链表初始化void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}

(4)构造函数

构造函数只需要创建一个哨兵位即可;

			// 构造函数list(){empty_init();}

(5)拷贝构造函数

拷贝构造函数直接初始化,然后插入数据即可;

			// 拷贝构造函数 -- lt2(lt1)list(const list<T>& lt){empty_init();for (auto e : lt){push_back(e);}}

(6)赋值运算符重载

现代写法,传参的时候调用拷贝构造,然后交换数据即可;

			// 赋值运算符重载 -- lt2 = lt1list<T>& operator=(list<T> lt){swap(lt);return *this;}

(7)析构函数

清空链表数据之后再释放哨兵位的节点即可;

			// 析构函数~list(){clear();delete _head;_head = nullptr;}

4. 打印容器的接口

(1)打印链表整型的接口

vectorlist 这些容器都没有重载流插入运算符,所以我们可以自己实现一个打印的接口函数;我们先来实现一下打印链表整型的接口:

		// 打印链表 -- 只能针对 int 类型void print_list(const list<int>& lt){list<int>::const_iterator it = lt.begin();while (it != lt.end()){//*it = 10; errorcout << *it << " ";++it;}cout << endl;}

此接口可以打印链表的数据,但是只能针对 int 类型,我们可以对它进行改造一下,使用模板。

(2)打印 list 的接口

我们学了模板,就可以利用模板实现泛型编程,将类型改为模板的泛型,即可打印 list 中的不同类型,如下:

		// 打印链表 -- 只能打印 list 容器template<typename T>void print_list(const list<T>& lt){typename list<T>::const_iterator it = lt.begin();while (it != lt.end()){//*it = 10; errorcout << *it << " ";++it;}cout << endl;}

这里的模板参数使用了 typedef 关键字,这里必须使用 typedef 关键字,而且在指定类域前还要加上 typedef 关键字,如 typename list<T>::const_iterator it = lt.begin();;因为在模板还没有进行实例化的时候, const_iterator 就到 list<T> 的类域中寻找类型,此时类中还没有实例化参数 T,所以编译器分不清它是类型还是静态变量,不能去 list<T> 里面找,所以在前面加 typedef 关键字就说明它是个类型,编译器在等 list<T> 实例化后,再去类里面去取根据类型去取类型。

但是上面的接口还是不够完美,要是我想打印 vector 呢?那还是不能打印出来,所以我们可以实现一个专门打印容器的接口;

(3)打印容器的接口

我们使用模板参数代表容器,让编译器到指定容器去取它的迭代器即可;

		// 打印容器 -- 能打印各种容器template<typename container>void print_container(const container& con){typename container::const_iterator cit = con.begin();while (cit != con.end()){cout << *cit << " ";++cit;}cout << endl;}

使用如下图:

在这里插入图片描述

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

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

相关文章

css3新增属性

文章目录 css3新增属性box-shadowborder-radius设置椭圆 position: sticky;渐变背景线性渐变可重复的渐变背景 径向渐变可重复的渐变背景 过渡分属性 动画关键帧与transition的关系demo 变形平移使用 旋转使用 其他使用立体效果perspective元素位于3D空间还是平面中 缩放变形的…

tornado在模板中遍历二维数组

要在Tornado模板中遍历一个二维数组&#xff0c;你可以使用Tornado的模板语法来实现迭代和显示数组中的每个元素。 以下是一个示例&#xff0c;演示如何在Tornado模板中遍历和显示二维数组的内容&#xff1a; template.html: <!DOCTYPE html> <html> <head&g…

小米分享 | 解密面试题:网易面试如何回答“创建线程有哪几种方式?”

大家好&#xff0c;我是你们的小米&#xff01;今天要和大家一起探讨一个在技术面试中常见的问题&#xff1a;创建线程有哪几种方式&#xff1f;这可是个经典面试题哦&#xff01;不过别担心&#xff0c;小米在这里为你详细解析&#xff0c;帮你轻松应对&#xff0c;让你在面试…

深度学习在MRI运动校正中的应用综述

运动是MRI中的主要挑战之一。由于MR信号是在频率空间中获取的&#xff0c;因此除了其他MR成像伪影之外&#xff0c;成像对象的任何运动都会导致重建图像中产生伪影。深度学习被提出用于重建过程的几个阶段的运动校正。广泛的MR采集序列、感兴趣的解剖结构和病理学以及运动模式&…

用dcker极简打包java.jar镜像并启动

用dcker极简打包java.jar镜像并启动 一、本地打包好jar包 二、新建文件夹&#xff0c;将步骤1中的jar包拷贝到文件夹下 三、同目录下新建Dockerfile ## 基础镜像&#xff0c;这里用的是openjdk:8 FROM openjdk:8## 将步骤一打包好的jar包 拷贝到镜像的 跟目录下[目录可以自定义…

Oracle字段长度不足位数补零

Oracle字段长度不足位数补零 有时候从数据库中取出的月份值是1&#xff0c;而不是01&#xff0c;该怎么办呢 SELECTLPAD( CODE_MONTH, 2, 0 ) FROMtb_cube_TY001 WHERECODE_BM_MEATYPE TY20 AND code_measure MYLX01 AND code_month <> ~ AND CODE_ENTITY 01A AND…

【实战】十一、看板页面及任务组页面开发(二) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二十四)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表三、TS 应用&#xff1a;JS神助攻 - 强类型四、JWT、用户认证与异步请求五、CSS 其实很简单 - 用 CSS-in-JS 添加样式六、用户体验优化 - 加载中和错误状态处理七、Hook&…

“深入探索JVM:解析Java虚拟机的工作原理与优化“

标题&#xff1a;深入探索JVM&#xff1a;解析Java虚拟机的工作原理与优化 摘要&#xff1a;本篇博客将深入探讨Java虚拟机&#xff08;JVM&#xff09;的工作原理以及如何优化JVM的性能。我们将介绍JVM的组成部分、类加载过程、内存管理、垃圾回收机制以及常见的性能优化技术…

记一次线上OOM事故

OOM 问题 linux内核有个机制叫OOM killer(Out-Of-Memory killer)&#xff0c;当系统需要申请内存却申请不到时&#xff0c;OOM killer会检查当前进程中占用内存最大者&#xff0c;将其杀掉&#xff0c;腾出内存保障系统正常运行。 一般而言&#xff0c;一个应用的内存逐渐增加&…

__setitem__和__getitem和__delitem__

目录 一、__setitem__ 二、__getitem__ 三、__delitem__与__delattr__ python从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129328397?spm1001.2014.3001.5502 class Foo:def __init__(self, name):self.name namedef __getitem__(s…

mekefile 编写

mekefile 编写 参考 Linux下使用 autoconf和automake 自动构建 项目 make file文件 makefile 中加入shell语句 if shell 参考 foo.bak: foo.barecho "foo"if [ -d "~/Dropbox" ]; then echo "Dir exists"; fi Or foo.bak: foo.barecho &quo…

文心一言 VS 讯飞星火 VS chatgpt (76)-- 算法导论7.3 1题

一、如果用go语言&#xff0c;为什么我们分析随机化算法的期望运行时间&#xff0c;而不是其最坏运行时间呢? 文心一言&#xff1a; 在分析算法的性能时&#xff0c;期望运行时间是一个重要的指标&#xff0c;它描述了算法在平均情况下的表现。期望运行时间考虑了所有可能的…

管理类联考——逻辑——真题篇——按知识分类——汇总篇——二、论证逻辑——支持加强——第二节——分类2——正面支持

文章目录 第二节 支持加强-分类2-正面支持题-支持加强-分类2-正面支持-表达“确实如此”题-支持加强-分类2-正面支持-表达“确实如此”-正面支持不直观:转为削弱反面更直观真题(2010-38)-支持加强-分类2-正面支持真题(2018-29)-支持加强-分类2-正面支持-支持关键词真题(…

musl libc ldso 动态加载研究笔记:02

前言 本篇继续研究 musl libc ldso 的动态加载过程中遇到的关键性的概念&#xff1a;到底要加载ELF 文件的哪些内容到 内存 当前如果遇到 ELF 动态加载&#xff0c;当前系统需要有【文件系统】&#xff0c;并且有较大的内存&#xff0c;因为 ELF 文件是无法直接运行的&#xf…

IDEA两种方法修改生成的jar包名字

方法一&#xff1a; 直接修改pom文件中的如下部分 <artifactId>excelreport</artifactId> <version>0.0.1-SNAPSHOT</version> <name>excelreport</name> <description>excelreport</description> 修改完成后&#xff0c;点…

SpringBoot3集成Kafka

标签&#xff1a;Kafka3.Kafka-eagle3&#xff1b; 一、简介 Kafka是一个开源的分布式事件流平台&#xff0c;常被用于高性能数据管道、流分析、数据集成和关键任务应用&#xff0c;基于Zookeeper协调的处理平台&#xff0c;也是一种消息系统&#xff0c;具有更好的吞吐量、内…

跟着美团学设计模式(感处)

读了着篇文章之后发现真的是&#xff0c;你的思想&#xff0c;你的思维是真的比比你拥有什么技术要强的。 注 开闭原则 开闭原则&#xff08;Open-Closed Principle&#xff09;是面向对象设计中的基本原则之一&#xff0c;它的定义是&#xff1a;一个软件实体应该对扩展开放…

python生成旗帜--比如美国国旗生成

目录 1、解释说明&#xff1a; 2、使用示例&#xff1a; 3、注意事项&#xff1a; 1、解释说明&#xff1a; 在Python中&#xff0c;生成国旗可以通过使用第三方库或者自定义函数来实现。通常&#xff0c;我们可以使用Pillow库来处理图像&#xff0c;以及使用matplotlib库来…

python爬虫7:实战1

python爬虫7&#xff1a;实战1 前言 ​ python实现网络爬虫非常简单&#xff0c;只需要掌握一定的基础知识和一定的库使用技巧即可。本系列目标旨在梳理相关知识点&#xff0c;方便以后复习。 申明 ​ 本系列所涉及的代码仅用于个人研究与讨论&#xff0c;并不会对网站产生不好…

carla中lka实现(二)

前言&#xff1a; 首先计算之前检测出来的车道线的中线与输入图像的中线进行计算距离&#xff0c;&#xff0c;并设置不同的阈值对于不同的方向进行相关的调整。 一、车辆中心线 一般而言将摄像头架设在车辆的正中心轴上&#xff0c;所获得的图像的中间线极为车辆的中心。 …