STL —— list

图片名称

博主首页: 有趣的中国人

专栏首页: C++专栏


    本篇文章主要讲解 list模拟实现的相关内容

    1. list简介


    列表(list)C++标准模板库(STL)中的一个容器,它是一个双向链表数据结构,用于存储元素。与vector不同,列表中的元素在内存中不是连续存储的,而是通过指针相互连接形成链表。这种设计使得列表对于插入和删除操作非常高效,但是在查找特定元素时相对较慢,因为需要按序遍历整个链表。


      2. list模拟实现

      2.1 list类的相关成员变量

      C++的标准库中,list的实现方式是带头双向循环链表,因此在类中,我们需要一个头指针_head。至于每个节点,我们也同样需要构造一个类,其中成员变量包含_prev_next和数据_val

      template<class T>
      struct ListNode
      {ListNode<T>* _prev;ListNode<T>* _next;T _val;ListNode(const T& x = T()):_prev(nullptr),_next(nullptr),_val(x){}
      };
      template<class T>
      class list
      {
      public:typedef ListNode<T> Node;list(){_head = new Node;_head->_prev = _head;_head->_next = _head;}
      private:Node* _head;
      };
      

        2.2 尾插

        尾插太简单了,直接上代码:

        void push_back(const T& x)
        {Node* newnode = new Node(x);Node* tail = _head->_prev;tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;
        }
        

          2.3 迭代器

          在库中,我们不管迭代器的底层是如何实现的,但是我们都要用相同的方法使用迭代器,例如之前讲过的 vectorstring,在g++中的实现方法就是原生指针,来实现例如++--等功能,但是这里list由于不是连续存储的,所以用原生指针正常的++--等功能并不能达到我们的预期,因此我们可以把迭代器搞成一个类类型,并用运算符重载来改变它的功能。

          下面的代码是迭代器正常的使用方法,我们需要用运算符重载来实现这些功能:
          void list_test1()
          {list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;
          }
          

            2.3.1 迭代器类的成员变量

            迭代器类中其实就包含了一个指向结点类型的指针,因为我们的目的就是改变原生指针的相关操作,来实现迭代器相关的操作。
            代码如下:

            struct ListNodeIterator
            {typedef ListNode<T> Node;Node* _node;ListNodeIterator(Node* node):_node(node){}
            };
            

              2.3.2 迭代器类的实现

              template<class T>
              struct ListNodeIterator
              {typedef ListNode<T> Node;typedef ListNodeIterator<T> Self;Node* _node;ListNodeIterator(Node* node):_node(node){}T& operator*(){return _node->_val;}Self& operator++(){_node = _node->_next;return *this;}Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& it){return _node != it._node;}
              };
              

                2.3.3 insert 和 erase

                inserterase传的参数就是iterator,模拟实现代码如下:

                void 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;
                }
                void erase(iterator pos)
                {Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;cur = nullptr;
                }
                
                这里需要注意,在erase的时候由于迭代器指向的空间被释放了,会导致迭代器失效的问题,之前的文章讲过相关的内容,因此我们需要更新iterator,指向被删除的位置的下一个位置即可,代码如下:
                iterator erase(iterator pos)
                {Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;cur = nullptr;return next;
                }
                

                  2.3.4 begin 和 end

                  beginend是在迭代器中的成员函数,返回头和尾的迭代器即可:

                  typedef ListNodeIterator<T> iterator;
                  iterator begin()
                  {return iterator(_head->_next);// 单参数类型的构造函数支持隐式类型转换,以下依法也可以:// return _head->_next;
                  }
                  iterator end()
                  {return iterator(_head);// return _head;
                  }
                  

                    2.3.5 insert 和 erase的复用

                    push_backpush_frontpop_backpop_front都可以复用inserterase,代码如下:

                    void push_back(const T& x)
                    {/*Node* newnode = new Node(x);Node* tail = _head->_prev;tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;*/insert(end(), x);
                    }
                    void pop_back()
                    {erase(--end());
                    }
                    void push_front(const T& x)
                    {insert(begin(), x);
                    }
                    void pop_front()
                    {erase(begin());
                    }
                    

                    测试代码:

                    void list_test1()
                    {list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;lt.pop_back();lt.pop_back();it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;lt.push_front(100);lt.push_front(200);lt.push_front(300);it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;lt.pop_front();lt.pop_front();it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;
                    }
                    

                    在这里插入图片描述


                      2.3.5 operator->的重载

                      • 先看一下这段代码:
                      void list_test2()
                      {struct A{int _a1;int _a2;A(int a1 = 0, int a2 = 0):_a1(a1),_a2(a2){}};list<A> lt;A a(1, 2);lt.push_back(a);lt.push_back(A(3, 4));lt.push_back({ 5,6 });list<A>::iterator it = lt.begin();while (it != lt.end()){// 主要看这里cout << (*it)._a1 << " " << (*it)._a2 << " ";cout << endl;++it;}
                      }
                      

                      我们注意到当我们访问自定义类型的数据需要这样:(*it)._a1进行访问,但是迭代器就是为了模仿指针的相关操作,例如我们有A*这种类型的指针如何进行访问A中的数据呢?

                      A* aa;
                      (*aa)._a1;
                      // 上面的方法很别扭,我们正常用指针都是用->访问的,所以我们如何实现->的重载呢?
                      aa->_a1;
                      
                      • 实现方法如下:
                      T* operator->()
                      {return &_node->_val;
                      }
                      

                      为什么这样就行了呢,我们知道自定义类型存储在了节点的_val中,这里返回了_val的地址,如果按照正常的思路进行访问,应该按照如下的方式:

                      • cout << it.operator->()->_a1 << " " << it.operator->()->_a2 << " ";

                      所以应该有两个箭头:第一个箭头代表运算符的重载,第二个代表指针解引用访问数据:

                      • cout << it->->_a1 << " " << it->->_a2 << " ";

                      但是编译器进行了简化,两个箭头变成了一个箭头

                      • cout << it->_a1 << " " << it->_a2 << " ";

                      `

                      void list_test2()
                      {struct A{int _a1;int _a2;A(int a1 = 0, int a2 = 0):_a1(a1),_a2(a2){}};/*A* aa;(*aa)._a1;aa->_a1;*/list<A> lt;A a(1, 2);lt.push_back(a);lt.push_back(A(3, 4));lt.push_back({ 5,6 });list<A>::iterator it = lt.begin();while (it != lt.end()){cout << (*it)._a1 << " " << (*it)._a2 << " ";cout << it.operator->()->_a1 << " " << it.operator->()->_a2 << " ";//cout << it->->_a1 << " " << it->->_a2 << " ";cout << it->_a1 << " " << it->_a2 << " ";cout << endl;++it;}
                      }
                      

                      在这里插入图片描述


                        2.4 const迭代器

                        • 我们先看以下这段代码:
                        void Print(const list<int>& lt)
                        {list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;
                        }void list_test3()
                        {list<int> lt;lt.push_back(1);lt.push_back(1);lt.push_back(8);lt.push_back(6);lt.push_back(0);Print(lt);
                        }
                        
                        • 很明显,会报错,提示不能从const转换为非const

                        在这里插入图片描述

                        • 很多人可能会想着在beginend后加上const进行修饰,但其实也不行,这样虽然传入的值是const类型,但是返回的值不是const类型,就会导致返回的值能被修改,但是要求是返回的值是const类型,所以这种想法是不行的,下面是错误的示范:
                        iterator begin() const
                        {return iterator(_head->_next);// 单参数类型的构造函数支持隐式类型转换,以下写法也可以:// return _head->_next;
                        }
                        iterator end() const
                        {return iterator(_head);// return _head;
                        }
                        void Print(const list<int>& lt)
                        {list<int>::iterator it = lt.begin();while (it != lt.end()){// 这里可以修改*it += 10;cout << *it << " ";++it;}cout << endl;
                        }void list_test3()
                        {list<int> lt;lt.push_back(1);lt.push_back(1);lt.push_back(8);lt.push_back(6);lt.push_back(0);Print(lt);
                        }
                        

                        在这里插入图片描述

                        • 那应该如何解决呢?在此之前,我们需要了解一下这段代码:
                        const iterator begin() 
                        {return iterator(_head->_next);// 单参数类型的构造函数支持隐式类型转换,以下写法也可以:// return _head->_next;
                        }
                        const iterator end() 
                        {return iterator(_head);// return _head;
                        }
                        
                        • 当我们在返回值前加上const,代表返回的迭代器不能被修改,例如不能进行++it
                        • 但是我们是想着迭代器指向的内容不能被修改,因此这种方法是不可行的。
                        • 可以类比一下这段代码:
                        // const修饰的是解引用之后的内容
                        const int* a;
                        // const修饰的是指针本身
                        int* const a;
                        
                        • 解决方法其实很简单,之前说过既然不满足要求,那我们就自己造轮子,自己写一个类;
                        • 这个类其实也很简单,就把ListNodeIterator这个类中的两个运算符重载函数的返回值改变一下就可以了,一个是,另一个是->
                        template<class T>
                        struct ListNodeConstIterator
                        {typedef ListNode<T> Node;typedef ListNodeConstIterator<T> Self;Node* _node;ListNodeConstIterator(Node* node):_node(node){}const T& operator*(){return _node->_val;}const T* operator->(){return &_node->_val;}Self& operator++(){_node = _node->_next;return *this;}Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& it){return _node != it._node;}
                        };
                        

                        list类中加入这两个函数:

                        const_iterator begin() const
                        {return const_iterator(_head->_next);// 单参数类型的构造函数支持隐式类型转换,以下写法也可以:// return _head->_next;
                        }const_iterator end() const
                        {return const_iterator(_head);// return _head;
                        }
                        
                        • 这时候就不能修改了

                        在这里插入图片描述
                        在这里插入图片描述


                          2.5 模板的作用

                          我们发现,在两个迭代器中,只用两个函数的返回值不同,其他的全部都一样,看上去非常冗余,那我们可不可以用一种方法来解决这种冗余呢?肯定是可以的,我们这个时候就可以用到模板:

                          template<class T, class Ref, class Ptr>
                          struct ListNodeIterator
                          {typedef ListNode<T> Node;typedef ListNodeIterator<T, Ref, Ptr> Self;Node* _node;ListNodeIterator(Node* node):_node(node){}Ref operator*(){return _node->_val;}Ptr operator->(){return &_node->_val;}Self& operator++(){_node = _node->_next;return *this;}Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& it){return _node != it._node;}
                          };
                          template<class T>
                          class list
                          {
                          public:typedef ListNode<T> Node;typedef ListNodeIterator<T, T&, T*> iterator;typedef ListNodeIterator<T,const T&, const T*> const_iterator;iterator begin() {return iterator(_head->_next);// 单参数类型的构造函数支持隐式类型转换,以下写法也可以:// return _head->_next;}iterator end() {return iterator(_head);// return _head;}const_iterator begin() const{return const_iterator(_head->_next);// 单参数类型的构造函数支持隐式类型转换,以下写法也可以:// return _head->_next;}const_iterator end() const{return const_iterator(_head);// return _head;}// ......
                          };
                          

                          这里虽然只写了一份iterator类,但是在编译的时候,编译器会根据你的需要生成两份iterator类,所以模板很强大。


                            3. 拷贝构造、赋值重载、析构

                            3.1 析构函数

                            • 析构函数释放掉空间即可,记住更新一下迭代器。
                            void clear()
                            {iterator it = begin();while (it != end()){it = erase(it);}
                            }~list()
                            {clear();delete _head;_head = nullptr;
                            }
                            

                            3.2 拷贝构造

                            • 拷贝构造新建一个头节点,然后尾插。
                            void empty_init()
                            {_head = new Node;_head->_prev = _head;_head->_next = _head;
                            }list(const list<T>& lt)
                            {empty_init();for (auto& e : lt){push_back(e);}
                            }
                            

                            3.3 赋值重载

                            • 赋值重载现代写法,之前讲过类似的方法:
                            void swap(list<T>& lt)
                            {std::swap(_head, lt._head);
                            }list<int>& operator=(list<T> lt)
                            {swap(lt);return *this;
                            }
                            

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

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

                            相关文章

                            StarRocks 面试题及参考答案详解(万字详解)

                            1. StarRocks 的架构设计是怎样的? 参考答案: StarRocks 的架构设计遵循了现代分布式系统的多个核心原则,以确保高性能和高可靠性。它主要由以下几个组件构成: FE(Frontend):作为前端服务,负责SQL的解析、编译和优化,以及执行计划的生成。BE(Backend):后端服务,负…

                            Linux从入门到精通 --- 2.基本命令入门

                            文章目录 第二章&#xff1a;2.1 Linux的目录结构2.1.1 路径描述方式 2.2 Linux命令入门2.2.1 Linux命令基础格式2.2.2 ls命令2.2.3 ls命令的参数和选项2.2.4 ls命令选项的组合使用 2.3 目录切换相关命令2.3.1 cd切换工作目录2.3.2 pwd查看当前工作目录2.4 相对路径、绝对路径和…

                            记录Linux系统中vim同时开多个窗口编辑文件

                            在使用Linux进行文本编辑的时候&#xff0c;通常使用vim编辑器编辑文件&#xff0c;当然啦&#xff0c;vim也可以创建文件&#xff0c;如果只是一个一个创建&#xff0c;只需要vim创建即可&#xff0c;但是如何一次性打开多个窗口编辑呢&#xff1f; 目录 1、目标&#xff1a;…

                            PHP数据类型

                            华子目录 数据类型PHP的八种数据类型基本数据类型&#xff0c;4类复合数据类型&#xff0c;2类特殊数据类型&#xff0c;2类 类型转换在PHP中有两种类型转换方式其他类型转bool类型其他类型转数值类型实例 类型判断获取和设定变量类型获取gettype(变量名)设置settype(变量名,类…

                            基于SSM+Jsp+Mysql的个性化影片推荐系统

                            开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包…

                            【SCI绘图】【曲线图系列2 python】多类别标签对比的曲线图

                            SCI&#xff0c;CCF&#xff0c;EI及核心期刊绘图宝典&#xff0c;爆款持续更新&#xff0c;助力科研&#xff01; 本期分享&#xff1a; 【SCI绘图】【曲线图系列2 python】多类别标签对比的曲线图&#xff0c;文末附完整代码。 1.环境准备 python 3 import proplot as pp…

                            基于离散差分法的复杂微分方程组求解matlab数值仿真

                            目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于离散差分法的复杂微分方程组求解.“连续微分方程”到“离散微分方程”到“差分方程”&#xff0c;离散微分方程&#xff0c;变成差分方程。建立差分方程时&am…

                            【Java EE】SpringBoot的创建与简单使用

                            文章目录 &#x1f340;环境准备&#x1f333;Maven&#x1f332;SpringBoot是什么&#x1f384;Spring Boot 项目创建&#x1f338;使用Idea创建&#x1f338;创建SpringBoot项⽬&#x1f338;SpringBoot项目的运行 ⭕总结 &#x1f340;环境准备 如果你的IDEA是专业版&#…

                            C++数据结构与算法——回溯算法分割问题

                            C第二阶段——数据结构和算法&#xff0c;之前学过一点点数据结构&#xff0c;当时是基于Python来学习的&#xff0c;现在基于C查漏补缺&#xff0c;尤其是树的部分。这一部分计划一个月&#xff0c;主要利用代码随想录来学习&#xff0c;刷题使用力扣网站&#xff0c;不定时更…

                            算法基本概念

                            算法基本概念 算法的定义 算法是解决特定问题求解步骤的描述&#xff0c;在计算机中表现为指令的有限序列&#xff0c;并且每条指令表示一个或多个操作。 算法的特性 输入&#xff1a;算法具有0个或多个输入输出&#xff1a;算法至少有一个或多个输出有穷性确定性可行性 算…

                            设计模式(14):命令模式

                            介绍 将一个请求封装为一个对象&#xff0c;从而使我们可用不同的请求对象客户进行参数化&#xff1b;对请求排队或者记录请求日志&#xff0c;以及支持可撤销的操作。也称之为&#xff1a;动作Action模式&#xff0c;事务transaction模式。 命令模式角色 抽象命令类(Comman…

                            VS CODE环境安装和hello world

                            SAP UI5 demo walkthrough tutorial step1 hello word 首先要安装nodejs,然后才能执行下面的操作 nodejs vscode 安装ui5npm install --global @ui5/cli报错解决: idealTree:npm: sill idealTree buildDeps 这个信息说明npm正在构建,如一直停留在这个界面,检查下网络,例如…

                            Redis常用命令补充和持久化

                            一、redis 多数据库常用命令 1.1 多数据库间切换 1.2 多数据库间移动数据 1.3 清除数据库内数据 1.4 设置密码 1.4.1 使用config set requirepass yourpassword命令设置密码 1.4.2 使用config get requirepass命令查看密码 二、redis高可用 2.1 redis 持久化 2.1.1 持…

                            【51单片机入门记录】A/D D/A转换器概述

                            目录 一、A/D D/A转换器简介 &#xff08;1&#xff09;模数转换器-ADC &#xff08;analogue-to-digital conversion&#xff09; &#xff08;2&#xff09;数模转换器-DAC&#xff08;digital-to-analogue conversion&#xff09; &#xff08;3&#xff09;应用场景 二…

                            用于AGV物流机器人的爱普生陀螺仪传感器XV7000系列

                            适用于AGV物流机器人的爱普生陀螺仪传感器XV7000系列:XV7001BB&#xff0c;XV7011BB。以前我们都知道XV7001BB&#xff0c;XV7011BB适用于扫地机器人&#xff0c;其实对于AGV物流机器人来说&#xff0c;XV7000系列生陀螺仪传感器也是其中重要一环。AGV机器人又叫做AGV搬运机器人…

                            C#手术麻醉信息系统源码,技术框架:Vue,Ant-Design+百小僧开源框架

                            C#手术麻醉信息系统源码&#xff0c;技术框架&#xff1a;Vue&#xff0c;Ant-Design百小僧开源框架 手术麻醉系统主要用于在手术过程中监测和控制患者的状态&#xff0c;确保手术的顺利进行并保障患者的生命安全。该系统通过一系列先进的医疗设备和技术&#xff0c;为手术患者…

                            AopContext.currentProxy() 的代理对象错误(未被更新)问题

                            背景&#xff1a; 原来在springAOP的用法中&#xff0c;只有代理的类才会被切入&#xff0c;我们在controller层调用service的方法的时候&#xff0c;是可以被切入的&#xff0c;但是如果我们在service层 A方法中&#xff0c;调用B方法&#xff0c;切点切的是B方法&#xff0c;…

                            Java 元素排序(数组、List 集合)

                            数组元素排序 升序 int[] array {3, 1, 4, 5}; Arrays.sort(array);// 升序排序 System.out.println(Arrays.toString(array)); //输出&#xff1a;[1, 3, 4, 5]降序 可以先将数组元素存入 List 集合&#xff0c;然后集合元素逆序&#xff0c;最后将集合元素写回原数组。&a…

                            助力第十五届蓝桥杯单片机省一——模块之eeprom

                            蓝桥杯比赛倒计时第8天 今天写的是蓝桥杯模块————AT24C02,主要讲一下如何对一个整型的数据进行存储&#xff0c;相信各位省一选手对一个char型的数组读取已经了如指掌了&#xff0c;所以就不再这里进行讲解了。 一、eeprom写入函数 void write_myepprom(unsigned char a…

                            JavaScript 事件流

                            JavaScript与HTML之间的交互是通过事件实现的&#xff0c;而用户与浏览器页面的互动也是通过事件来实现的事件就是文档或浏览器窗口中发生的一些特定的交互瞬间&#xff0c;所以分为两种事件&#xff0c;一是发生在 浏览器对象&#xff08;BOM&#xff09;上的事件&#xff0c;…