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

C++初阶 | [九] list 及 其模拟实现

摘要:介绍 list 容器,list 模拟实现,list与vector的对比


list(带头双向循环列表)

导入:list 的成员函数基本上与 vector 类似,具体内容可以查看相关文档(cplusplus.com/reference/list/list/),这里不多赘述。以下对 list 的 Operations 部分的函数进行简单讲解。

Operations:

splice

Transfer elements from list to list (public member function)

remove

Remove elements with specific value (public member function)

remove_if

Remove elements fulfilling condition (public member function template)

unique

Remove duplicate values (public member function)

merge

Merge sorted lists (public member function)

sort

Sort elements in container (public member function)

reverse

Reverse the order of elements (public member function)

注意:list 没有扩容的概念,而是一份一份相对独立的节点串连起来的。

1)sort

  • #include<list> std::list::sort#include<algorithm> std::sort
    如上图,RandomAccessIterator 至少已经在名称上提示使用者,这个 sort 函数要求支持能够被随机访问的迭代器
    首先,list 的迭代器是双向迭代器;其次,从底层实现来看,std::sort 函数用到了迭代器相减,而 list 的地址是不连续的。所以 list 不支持使用 std::sort 函数。

  •  std::list::sort 的使用:该函数默认升序排列(底层是归并排序)
    如果要降序排序有如下代码以供参考:(std::greater<int>() 是一个 greater 类型的匿名对象,这种写法更常用)

    #include<functional>
    #include<list>int main()
    {std::list<int> lt;//在 lt 中插入一些数据之后std::greater<int> gt;lt.sort(gt);//or:lt.sort(std::greater<int>());return 0;
    }

  • std::list::sort 性能测试
    测试结果:
    ①在 Rlease 模式下, std::vector::sort 效率大约是 list 的 2 倍,并且数据量越大效率差距越大。(tip.性能测试要在 Rlease 模式下进行,Debug 模式下优化没有全开)
    ②通过 vector 给 list 排序:把 list 对象 → 拷贝数据到 vector 对象中 →对 vector 对象 sort → 把排序好的数据拷贝到 list 。这样对 list 排序,在数据量较大的情况下效率甚至比 list 直接排序要高。

sumlist 的 sort 在性能上没有什么优势,list 中的 sort 函数在对于数据量小的情况下可以使用,但平时能不用尽量不要频繁使用。

2)merge

归并两个 list 到一个 list(要先 sort 才可以 merge,实践中很少用)。

3)unique

去重,但也有要求——只能去除连续相同的,所以要先 sort 再 unique 才可以真正“去重”。

4)splice

转移(移动指针),如下图。

以上就是对 list 一些函数的简单介绍。 


list 的模拟实现

1)结构

如上图,list 中的每个节点是一个自定义类型 Node,对于双向链表,每个节点内包括自身储存的数据、前节点指针和后节点指针。
对于由一个一个节点组成的 list通过头节点来管理整个 list

代码示例

// List的节点类template<class T>struct ListNode{ListNode<T>* _pPre;ListNode<T>* _pNext;T _val;};//List类template<class T>class list{PNode _pHead;//注意:这里是一个内置类型(指针)	};

2)初始化_Constructor

对 list 的初始化首先是对头节点的初始化。

// List的节点类template<class T>struct ListNode{ListNode(const T& val = T()): _val(val), _pPre(nullptr), _pNext(nullptr){}ListNode<T>* _pPre;ListNode<T>* _pNext;T _val;};//List类template<class T>class list{typedef ListNode<T> Node;typedef Node* PNode;public:///// List的构造list(){CreateHead();}private:void CreateHead()//对头结点进行初始化{_pHead = new Node;//这里会去调用struct ListNode的构造函数_pHead->_pNext = _pHead;_pHead->_pPre = _pHead;}PNode _pHead;//注意:这里是一个内置类型(指针)	};

3)Iterator

class Iterator——Iterator类

  1. 成员变量:Node* _pNode
  2. 成员函数:operator* 、operator++ 、operator-- 、operator!= 、operator==(模拟指针的行为)——这里体现了“封装”。封装屏蔽底层差异和实现细节,提供统一的访问修改遍历方式。

代码示例

	//List的迭代器类template<class T>class ListIterator{typedef ListNode<T>* PNode;typedef ListIterator<T> Self;public://constructorListIterator(PNode pNode = nullptr):_pNode(pNode){}ListIterator(const Self& l)//copy constructor{_pNode = l._pNode;}//operationsT& operator*(){return _pNode->_val;}T* operator->(){return &_pNode->_val;}Self& operator++(){_pNode = _pNode->_pNext;return *this;}Self operator++(int){Self tmp = _pNode;_pNode = _pNode->_pNext;return tmp;}Self& operator--(){_pNode = _pNode->_pPre;return *this;}Self operator--(int){Self tmp = _pNode;_pNode = _pNode->_pPre;return tmp;}bool operator!=(const Self& l){return _pNode != l._pNode;}bool operator==(const Self& l){return _pNode == l._pNode;}PNode _pNode;};

对 operator-> 的补充说明

我们知道,对于自定义类型,可以通过对其指针解引用 " *(pointer). " 和 " (pointer)-> " 来访问其成员。而 iterator 实际上是在模拟指针的行为,对于 operator-> 的使用编译器做出了优化。如下图。

3)Const_Iterator 

注意!const_iterator 不是用 const 修饰 iterator,如上 iterator 中的模拟实现可以看出,iterator 底层是原生指针,用 const 修饰 iterator 是使得指针本身不可修改,const_iterator 本身是要能被进行 ++ 和 -- 操作的,否则无法实现遍历;而 const_iterator 针对的是被 const 修饰的 list 的对象,即 const 修饰的是 list 的实例化对象本身(ps. list 对象是 const 的,那储存在节点中的数据肯定也是 const 的,即为 const T)

如上图,实际上我们需要实现两个不同的 iterator —— class ListIteratorclass ListConst_Iterator ,而对于 const 对象,begin 和 end 函数将会返回 const_iterator。

优化:使用类模板实现 List 的 Iterator 类

代码示例

	//List的迭代器类template<class T, class Ref, class Ptr>class ListIterator{typedef ListNode<T>* PNode;typedef ListIterator<T, Ref, Ptr> Self;public://constructorListIterator(PNode pNode = nullptr):_pNode(pNode){}ListIterator(const Self& l)//copy constructor{_pNode = l._pNode;}//operationsRef operator*(){return _pNode->_val;}Ptr operator->(){return &_pNode->_val;}Self& operator++(){_pNode = _pNode->_pNext;return *this;}Self operator++(int){Self tmp = _pNode;_pNode = _pNode->_pNext;return tmp;}Self& operator--(){_pNode = _pNode->_pPre;return *this;}Self operator--(int){Self tmp = _pNode;_pNode = _pNode->_pPre;return tmp;}bool operator!=(const Self& l){return _pNode != l._pNode;}bool operator==(const Self& l){return _pNode == l._pNode;}PNode _pNode;};//list类template<class T>class list{typedef ListNode<T> Node;typedef Node* PNode;public:typedef ListIterator<T, T&, T*> iterator;typedef ListIterator<T, const T&, const T&> const_iterator;public:///// List的构造list(){CreateHead();}///// List Iteratoriterator begin(){return _pHead->_pNext;}iterator end(){return _pHead;}const_iterator begin() const{return _pHead->_pNext;}const_iterator end()const{return _pHead;}}

注意:同一个类模板,实例化参数不同,就是完全不同的类型,即对于 ListIterator<T, T&, T*> 和 ListIterator<T, const T&, const T&> 是两个不同的类型。(ps. iterator 和 const_iterator 都实现之后才可以支持使用范围 for)

4)其他成员函数

这些成员函数实现起来思路很简单,有问题建议去看数据结构的文章回顾一下。以下简略说明。

①insert

insert 之后 iterator 不失效,因为没有扩容的影响。

		// 在pos位置前插入值为val的节点iterator insert(iterator pos, const T& val){PNode cur = pos._pNode;PNode newnode = new Node(val);newnode->_pNext = cur;newnode->_pPre = cur->_pPre;cur->_pPre = newnode;newnode->_pPre->_pNext = newnode;return pos;}

②erase

erase 之后 iterator 失效,因为这个被 erase 的节点被释放了,那么指向它的 iterator 也就失效了。

		// 删除pos位置的节点,返回该节点的下一个位置iterator erase(iterator pos){if (!empty()){PNode next = pos._pNode->_pNext;pos._pNode->_pPre->_pNext = next;next->_pPre = pos._pNode->_pPre;delete pos._pNode;--_size;return next;}return _pHead;}

③push_back and push_front

复用 insert。

		// List Modifyvoid push_back(const T& val) { insert(end(), val); }void push_front(const T& val) { insert(begin(), val); }	

④pup_back and pop_front

复用 erase。

		// List Modifyvoid pop_back() { erase(--end()); }void pop_front() { erase(begin()); }

⑤clear

用 iterator 遍历,依次 erase 每个节点。

		void clear(){iterator it = begin();while (it != end()){it = erase(it);}}

⑥Destructor

clear → delete → nullptr,即清理 list,释放头节点,头结点指针指针置空。

		//destructor~list(){clear();delete _pHead;_pHead = nullptr;}

⑦Copy Constructor

范围 for 循环 push_back。(注意:使用范围 for 需要把 const_iterator 也实现了才能用)

		list(const list<T>& l)//copy constructor{CreateHead();for (auto e : l){push_back(e);}}

⑧赋值重载

		//assignlist<T>& operator=(list<T> l){if (_pHead != l._pHead){swap(l);return *this;}}void swap(list<T>& l){std::swap(_pHead, l._pHead);std::swap(_size, l._size);}

⑨其他构造函数重载

		list(int n, const T& value = T()){CreateHead();while (n--){push_back(value);}}template <class Iterator>list(Iterator first, Iterator last){CreateHead();Iterator it = first;while (it != last){push_back(*it);++it;}}

补充:list 的成员变量中可以加一个 size_t 类型的变量来记录节点个数,因为如果没有这个成员变量就需要遍历来获取有效数据个数,效率比较低。(提醒:如果增加了 size_t 类型的成员变量记得在 insert 和 erase 的函数实现中相应地做出调整)

5)补充:Print

针对于 list<int> / list<char> 等类型的打印函数很好实现,以下我们尝试写出更通用的打印函数。

打印 list<T> 而不只是针对某个具体的 T 类型

因为语法编译之前要先对模板进行实例化,对于 Btl::list<T>::const_iterator 由于模板没有被实例化,所以编译器不知道 const_iterator  list<T> 中的一个内嵌类型还是静态成员变量,这样的行为对于编译器是未知的。

所以,Btl::list<T>::const_iterator 前加 typename 来声明这是一个内嵌类型。代码如下。

template<typename T>
void print_l(const Btl::list<T>& _list)
{typename Btl::list<T>::const_iterator it = _list.begin();while (it != _list.end()){std::cout << *it;++it;}std::cout << std::endl;
}

打印任意容器

提醒:下列代码中要求 *it 支持流插入。

template<typename Container>
void print_l(const Container& _con)
{typename Container::const_iterator it = _con.begin();while (it != _con.end()){std::cout << *it;++it;}std::cout << std::endl;
}

回顾:vector模拟实现中涉及的深浅拷贝的问题

对于类似 vector<string> 而出现的深浅拷贝问题,因为 list 不涉及扩容的概念,所以不会出现深浅拷贝的问题。


list与vector的对比

vectorlist
底层结构动态顺序表,一段连续空间带头结点的双向循环链表
随机访问支持随机访问,访问某个元素的效率为O(1)不支持随机访问,访问某个元素的效率为O(N)
插入和删除任意位置插入和删除效率低,需要搬移元素(挪动数据),时间复杂度为O(N),插入时有可能需要增容——开辟新空间,拷贝元素,释放就空间,导致效率更低任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1)
空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高底层结点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器原生态指针对原生态指针(节点指针)进行封装
迭代器失效

在插入元素时,要给所有迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效;删除时,当前迭代器需要重新给赋值否则会失效

插入元素不会导致迭代器失效;删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景需要高效存储,支持随机访问,不关系插入删除效率大量插入和删除操作,不关心随机访问

完整代码链接My_List/My_List/My_List.h · fantansy-13-07/Cpp - 码云 - 开源中国 (gitee.com)


END

相关文章:

C++初阶 | [九] list 及 其模拟实现

摘要&#xff1a;介绍 list 容器&#xff0c;list 模拟实现&#xff0c;list与vector的对比 list&#xff08;带头双向循环列表&#xff09; 导入&#xff1a;list 的成员函数基本上与 vector 类似&#xff0c;具体内容可以查看相关文档(cplusplus.com/reference/list/list/)&…...

如何将Excel两列数据转换为统计图、曲线图、折线图?如何自定义某一列作为Excel的统计图横纵坐标?

这样&#xff0c;横坐标就更换为指定选中的数据了 我们还可以修改统计图的样式 也可以修改统计图的类型...

[HackMyVM] Quick

kali:192.168.56.104 主机发现 arp-scan -l # arp-scan -l Interface: eth0, type: EN10MB, MAC: 00:0c:29:d2:e0:49, IPv4: 192.168.56.104 Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan) 192.168.56.1 0a:00:27:00:00:05 (Un…...

算法体系-12 第 十二 二叉树的基本算法

一 实现二叉树的按层遍历 1.1 描述 1&#xff09;其实就是宽度优先遍历&#xff0c;用队列 2&#xff09;可以通过设置flag变量的方式&#xff0c;来发现某一层的结束&#xff08;看题目&#xff09;看下边的第四题解答 1.2 代码 public class Code01_LevelTraversalBT {publ…...

【论文笔记合集】LSTNet之循环跳跃连接

本文作者&#xff1a; slience_me LSTNet 循环跳跃连接 文章仅作为个人笔记 论文链接 文章原文 LSTNet [25] introduces convolutional neural networks (CNNs) with recurrent-skip connections to capture the short-term and long-term temporal patterns. LSTNet [25]引入…...

数据库关系运算理论:关系数据操作与关系完整性概念解析

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…...

Linux基础开发工具之yum与vim

1. Linux软件包管理器——yum 1.1 什么是软件包&#xff1f; 在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程序. 但是这样太麻烦了, 于是有些人把一些常用的软件提前编译好, 做成软件包(可以理解成windows上的安装程序)放在一个服务器上, …...

【正则表达式】正则表达式里使用变量

码 const shuai No My Name Is ShuaiGe.match(new RegExp(shuai, gi)); //↑↑↑↑↑↑↑↑ //等同于 //↓↓↓↓↓↓↓↓ /No/gi.test(My Name Is ShuaiGe)用作领域 搜索的字符动态改变&#xff0c;例如↓模糊搜索例&#xff1a; 一个文本宽&#xff0c;输入文本模糊搜索用…...

Java中的可变参数

java提供了可变参数这个语法。 可变参数本质为数组。 一般可变参数应用于形参中。用于接收实参。 此时实参可以有多种形式。 一种是最正常的&#xff0c;实参为数组名。 public class Date1 {public void one(int ... arr){int sum0;for (int x:arr){sumx;}System.out.pri…...

如何实现在固定位置的鼠标连点

鼠大侠的鼠标连点功能是免费的 浏览器搜索下载鼠大侠&#xff0c;指定连点间隔和启动快捷键 点击设置&#xff0c;指定点击位置...

15|BabyAGI:根据气候变化自动制定鲜花存储策略

一种新型的代理——Autonomous Agents&#xff08;自治代 理或自主代理&#xff09;&#xff0c; 在 LangChain 的代理、工具和记忆这些组件的支持下&#xff0c;它们能够在无需外部干预的情况下自主 运行&#xff0c;这在真实世界的应用中具有巨大的价值。 AutoGPT 它的主要…...

二进制安全找实习记录

就安全岗而言&#xff0c;这里笔者仅仅面试了腾讯的科恩实验室内核安全和浏览器安全&#xff08;其它的就面了一下前后端开发&#xff0c;这就不说了&#xff0c;笔者也没打算搞开发&#xff09;&#xff0c;然后倒在了一面。然后有的问题忘记了&#xff0c;仅仅记录一下自己回…...

列表(list)篇(一)

文章目录 2.1 创建列表2.2 append()函数2.3 clear()函数2.4 copy()函数2.5 count()函数2.6 del2.7 enumerate()函数2.8 extend()函数2.9 index()函数 2.1 创建列表 在Python中&#xff0c;列表&#xff08;list&#xff09;是一种基础的数据结构&#xff0c;可以包含不同类型的…...

spring整合Sentinel

安装sentinel&#xff1a; 执行命令; java -jar sentinel-dashboard-1.8.6.jar 注:sentinel的默认端口为8080&#xff0c;容易出现tomcat的冲突。 当端口冲突&#xff0c;可以使用该指令修改sentinel的端口 默认账号和密码都为sentinel Springcloud整合sentinel&#xff1a;…...

MFC 自定义分发消息方法

重点&#xff1a; 1.创建一个专门自定义消息的头文件 constValue.h #define WM_MY_CUSTOM_MESSAGE (WM_USER 101) // 自定义消息ID 2.在你需要发送和接收该消息的类中&#xff0c;首先注册这个自定义消息。一般在窗口类&#xff08;如CWnd派生类&#xff09;的OnInitDialog…...

FPGA的应用方向

文章目录 FPGA是什么&#xff1f;FPGA的发展FPGA有哪些公司国内的FPGA发展如何&#xff1f;国内FPGA应用情况怎样&#xff1f;FPGA的发展方向有哪些&#xff1f;FPGA在工业界的应用有哪些&#xff1f;FPGA在科研界的方向有哪些&#xff1f;FPGA在高频信号处理的应用场景FPGA应用…...

河南大学大数据平台技术实验报告二

大数据平台技术课程实验报告 实验二&#xff1a;HDFS操作实践 姓名&#xff1a;杨馥瑞 学号&#xff1a;2212080042 专业&#xff1a;数据科学与大数据技术 年级&#xff1a;2022级 主讲教师&#xff1a;林英豪 实验时间&#xff1a;2024年3月15日3点 至 2024年3月15日4点40 …...

Spring Cloud Gateway如何实现熔断

Spring Cloud Gateway熔断集成 熔断应用&#xff1a; 金融市场中的熔断机制&#xff1a;在金融交易系统中&#xff0c;熔断机制&#xff08;Circuit Breaker&#xff09;是一种市场保护措施&#xff0c;旨在预防市场剧烈波动时可能导致的系统性风险。当某个基准指数&#xff08…...

2403d,d的com哪里错了

原文 感谢任意见解.细节: >dmd --version DMD64 D Compiler v2.107.0参考: ComObject类 IUnknown接口 我只使用了ComObject类和隐式继承了IUnknown接口,用用ImportC编译并包含以下内容的comheaders.c编写了一些COM测试代码. #define WINVER 0x0A00 #define _WIN32_WINNT…...

LeetCode151:反转字符串中的单词

题目描述 给你一个字符串 s &#xff0c;请你反转字符串中 单词 的顺序。 单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。 返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。 注意&#xff1a;输入字符串 s中可能会存在前导空…...

Vue记事本应用实现教程

文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展&#xff1a;显示创建时间8. 功能扩展&#xff1a;记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

如何在看板中体现优先级变化

在看板中有效体现优先级变化的关键措施包括&#xff1a;采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中&#xff0c;设置任务排序规则尤其重要&#xff0c;因为它让看板视觉上直观地体…...

解锁数据库简洁之道:FastAPI与SQLModel实战指南

在构建现代Web应用程序时&#xff0c;与数据库的交互无疑是核心环节。虽然传统的数据库操作方式&#xff08;如直接编写SQL语句与psycopg2交互&#xff09;赋予了我们精细的控制权&#xff0c;但在面对日益复杂的业务逻辑和快速迭代的需求时&#xff0c;这种方式的开发效率和可…...

Java - Mysql数据类型对应

Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...

Nuxt.js 中的路由配置详解

Nuxt.js 通过其内置的路由系统简化了应用的路由配置&#xff0c;使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...

Python爬虫(一):爬虫伪装

一、网站防爬机制概述 在当今互联网环境中&#xff0c;具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类&#xff1a; 身份验证机制&#xff1a;直接将未经授权的爬虫阻挡在外反爬技术体系&#xff1a;通过各种技术手段增加爬虫获取数据的难度…...

智能仓储的未来:自动化、AI与数据分析如何重塑物流中心

当仓库学会“思考”&#xff0c;物流的终极形态正在诞生 想象这样的场景&#xff1a; 凌晨3点&#xff0c;某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径&#xff1b;AI视觉系统在0.1秒内扫描包裹信息&#xff1b;数字孪生平台正模拟次日峰值流量压力…...

深度学习习题2

1.如果增加神经网络的宽度&#xff0c;精确度会增加到一个特定阈值后&#xff0c;便开始降低。造成这一现象的可能原因是什么&#xff1f; A、即使增加卷积核的数量&#xff0c;只有少部分的核会被用作预测 B、当卷积核数量增加时&#xff0c;神经网络的预测能力会降低 C、当卷…...

CSS设置元素的宽度根据其内容自动调整

width: fit-content 是 CSS 中的一个属性值&#xff0c;用于设置元素的宽度根据其内容自动调整&#xff0c;确保宽度刚好容纳内容而不会超出。 效果对比 默认情况&#xff08;width: auto&#xff09;&#xff1a; 块级元素&#xff08;如 <div>&#xff09;会占满父容器…...

在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案

这个问题我看其他博主也写了&#xff0c;要么要会员、要么写的乱七八糟。这里我整理一下&#xff0c;把问题说清楚并且给出代码&#xff0c;拿去用就行&#xff0c;照着葫芦画瓢。 问题 在继承QWebEngineView后&#xff0c;重写mousePressEvent或event函数无法捕获鼠标按下事…...