C++初阶 | [九] list 及 其模拟实现
摘要:介绍 list 容器,list 模拟实现,list与vector的对比
list(带头双向循环列表)
导入:list 的成员函数基本上与 vector 类似,具体内容可以查看相关文档(cplusplus.com/reference/list/list/),这里不多赘述。以下对 list 的 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 直接排序要高。
sum. list 的 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类
- 成员变量:Node* _pNode
- 成员函数: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 ListIterator 和 class 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的对比
vector | list | |
底层结构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问,访问某个元素的效率为O(1) | 不支持随机访问,访问某个元素的效率为O(N) |
插入和删除 | 任意位置插入和删除效率低,需要搬移元素(挪动数据),时间复杂度为O(N),插入时有可能需要增容——开辟新空间,拷贝元素,释放就空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层结点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭代器失效 | 在插入元素时,要给所有迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效;删除时,当前迭代器需要重新给赋值否则会失效 | 插入元素不会导致迭代器失效;删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
使用场景 | 需要高效存储,支持随机访问,不关系插入删除效率 | 大量插入和删除操作,不关心随机访问 |
附
完整代码链接My_List/My_List/My_List.h · fantansy-13-07/Cpp - 码云 - 开源中国 (gitee.com)
END
相关文章:

C++初阶 | [九] list 及 其模拟实现
摘要:介绍 list 容器,list 模拟实现,list与vector的对比 list(带头双向循环列表) 导入:list 的成员函数基本上与 vector 类似,具体内容可以查看相关文档(cplusplus.com/reference/list/list/)&…...

如何将Excel两列数据转换为统计图、曲线图、折线图?如何自定义某一列作为Excel的统计图横纵坐标?
这样,横坐标就更换为指定选中的数据了 我们还可以修改统计图的样式 也可以修改统计图的类型...

[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)其实就是宽度优先遍历,用队列 2)可以通过设置flag变量的方式,来发现某一层的结束(看题目)看下边的第四题解答 1.2 代码 public class Code01_LevelTraversalBT {publ…...

【论文笔记合集】LSTNet之循环跳跃连接
本文作者: 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的博文(づ ̄3 ̄)づ╭❤~✨✨ 🌟🌟 欢迎各位亲爱的读者,感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢,在这里我会分享我的知识和经验。&am…...

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

【正则表达式】正则表达式里使用变量
码 const shuai No My Name Is ShuaiGe.match(new RegExp(shuai, gi)); //↑↑↑↑↑↑↑↑ //等同于 //↓↓↓↓↓↓↓↓ /No/gi.test(My Name Is ShuaiGe)用作领域 搜索的字符动态改变,例如↓模糊搜索例: 一个文本宽,输入文本模糊搜索用…...
Java中的可变参数
java提供了可变参数这个语法。 可变参数本质为数组。 一般可变参数应用于形参中。用于接收实参。 此时实参可以有多种形式。 一种是最正常的,实参为数组名。 public class Date1 {public void one(int ... arr){int sum0;for (int x:arr){sumx;}System.out.pri…...

如何实现在固定位置的鼠标连点
鼠大侠的鼠标连点功能是免费的 浏览器搜索下载鼠大侠,指定连点间隔和启动快捷键 点击设置,指定点击位置...

15|BabyAGI:根据气候变化自动制定鲜花存储策略
一种新型的代理——Autonomous Agents(自治代 理或自主代理), 在 LangChain 的代理、工具和记忆这些组件的支持下,它们能够在无需外部干预的情况下自主 运行,这在真实世界的应用中具有巨大的价值。 AutoGPT 它的主要…...
二进制安全找实习记录
就安全岗而言,这里笔者仅仅面试了腾讯的科恩实验室内核安全和浏览器安全(其它的就面了一下前后端开发,这就不说了,笔者也没打算搞开发),然后倒在了一面。然后有的问题忘记了,仅仅记录一下自己回…...
列表(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中,列表(list)是一种基础的数据结构,可以包含不同类型的…...

spring整合Sentinel
安装sentinel: 执行命令; java -jar sentinel-dashboard-1.8.6.jar 注:sentinel的默认端口为8080,容易出现tomcat的冲突。 当端口冲突,可以使用该指令修改sentinel的端口 默认账号和密码都为sentinel Springcloud整合sentinel:…...
MFC 自定义分发消息方法
重点: 1.创建一个专门自定义消息的头文件 constValue.h #define WM_MY_CUSTOM_MESSAGE (WM_USER 101) // 自定义消息ID 2.在你需要发送和接收该消息的类中,首先注册这个自定义消息。一般在窗口类(如CWnd派生类)的OnInitDialog…...
FPGA的应用方向
文章目录 FPGA是什么?FPGA的发展FPGA有哪些公司国内的FPGA发展如何?国内FPGA应用情况怎样?FPGA的发展方向有哪些?FPGA在工业界的应用有哪些?FPGA在科研界的方向有哪些?FPGA在高频信号处理的应用场景FPGA应用…...

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

Spring Cloud Gateway如何实现熔断
Spring Cloud Gateway熔断集成 熔断应用: 金融市场中的熔断机制:在金融交易系统中,熔断机制(Circuit Breaker)是一种市场保护措施,旨在预防市场剧烈波动时可能导致的系统性风险。当某个基准指数(…...
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 ,请你反转字符串中 单词 的顺序。 单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。 返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。 注意:输入字符串 s中可能会存在前导空…...

Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...

SpringTask-03.入门案例
一.入门案例 启动类: package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...
JavaScript 数据类型详解
JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型(Primitive) 和 对象类型(Object) 两大类,共 8 种(ES11): 一、原始类型(7种) 1. undefined 定…...
WebRTC从入门到实践 - 零基础教程
WebRTC从入门到实践 - 零基础教程 目录 WebRTC简介 基础概念 工作原理 开发环境搭建 基础实践 三个实战案例 常见问题解答 1. WebRTC简介 1.1 什么是WebRTC? WebRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音…...

毫米波雷达基础理论(3D+4D)
3D、4D毫米波雷达基础知识及厂商选型 PreView : https://mp.weixin.qq.com/s/bQkju4r6med7I3TBGJI_bQ 1. FMCW毫米波雷达基础知识 主要参考博文: 一文入门汽车毫米波雷达基本原理 :https://mp.weixin.qq.com/s/_EN7A5lKcz2Eh8dLnjE19w 毫米波雷达基础…...

Golang——7、包与接口详解
包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...