【C++修炼秘籍】List深度剖析

【C++修炼秘籍】STL-List

☀️心有所向,日复一日,必有精进

☀️专栏《C++修炼秘籍》

☀️作者:早凉

☀️如果有错误,烦请指正,如有疑问可私信联系;


目录

【C++修炼秘籍】STL-List

文章目录

前言

一、list介绍

二、list的使用/接口介绍

构造函数

list iterator的使用

list capacity

list element access

list modifiers

三、list模拟实现

 结点定义

list类(构造函数和基本函数)

迭代器

增删查改 

迭代器失效问题

拷贝构造

 赋值运算符重载

size

有关类名和类型的问题

operator->

list和vector

总结


前言

相较于vector连续线性空间,list就显示的复杂很多,相较于vector,list既有优势也有缺点,两者并无好坏之分,只是各有其适用范围。首先,list的一个好处就是插入删除一个元素,就增加或释放一个元素空间,对空间的利用效率是极高的,对于任意位置的插入删除,list永远是O(1)的时间复杂度。

list和vector作为两个最常被使用的容器,什么时机去使用,也是一个比较复杂的问题,随着文章的深入,会慢慢对这个问题做出解答。


一、list介绍

//list node
template <class T>
struct __list_node {
  typedef void* void_pointer;
  void_pointer next;
  void_pointer prev;
  T data;
};

从标准模板库中我们观察链表结点,可以明显看出,list底层是带头双向循环链表,不支持随机访问

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

二、list的使用/接口介绍

构造函数

构造函数( (constructor))接口说明
list (size_type n, const value_type& val = value_type())构造的list中包含n个值为val的元素
list()构造空的list
list (const list& x)拷贝构造函数
list (InputIterator first, InputIterator last)用[first, last)区间中的元素构造list

list iterator的使用

函数声明接口说明
begin +
end
返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rbegin +
rend
返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的
reverse_iterator,即begin位置

迭代器就是一个类似于指针的行为,虽然底层实现不是原生指针。但是上层我们可以暂时理解如此。

list capacity

函数声明接口说明
empty检测list是否为空,是返回true,否则返回false
size返回list中有效节点的个数

list element access

函数声明接口说明
front返回list的第一个节点中值的引用
back返回list的最后一个节点中值的引用

list modifiers

函数声明接口说明
push_front在list首元素前插入值为val的元素
pop_front删除list中第一个元素
push_back在list尾部插入值为val的元素
pop_back删除list中最后一个元素
insert在list position 位置中插入值为val的元素
erase删除list position位置的元素
swap交换两个list中的元素
clear清空list中的有效元素

三、list模拟实现

 结点定义

这是模式实现的一个结点,其实我们使用class和struct均可,实现一个构造函数。 

    template<class T> 
	struct list_node{
		T _val;
		list_node<T>*_prev;
		list_node<T>*_next;
		list_node(const T& x)
			:_prev(nullptr),
			_next(nullptr),
			_val(x){}
	};

list类(构造函数和基本函数)

 先暂时实现一个list类运行起来,这里其实是使用__list_iterator<T>是一个类,封装迭代器,简单实现push_back,为下面的迭代器实现做准备。

template<class T>
	class list{
	public:
		typedef list_node<T> node;
		typedef __list_iterator<T> iterator;
        //typedef __list_const_iterator<T> const_iterator;
		list()//默认构造函数
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}
        template <class InputIterator>
		list(InputIterator first, InputIterator last)//迭代器构造函数
		{
			empty_initialize();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		iterator begin()
		{
			return iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		void push_back(T val)
		{
			//新建结点
			node* NewNode = new node(val);
			node* tail = _head->_prev;
			tail->_next = NewNode;
			NewNode->_prev = tail;
			NewNode->_next = _head;
			_head->_prev = NewNode;
		}
		void insert(iterator pos, T val)
		{

		}
	private:
		node*_head;
	};

迭代器

为什么这里要单独解析list的迭代器,在上文vector的迭代器我们使用的是原生指针,是因为vector是一块连续的线性空间,而list不保证结点空间连续,那么如何使得list的迭代器能像vector进行相同的上层操作,忽视底层细节,这是一个很大的问题。我们只有解决了迭代器问题才能解决模拟实现的大部分问题。

list<int>::iterator it = It.begin();

while(it!=It.end())
{
	cout<<*it<<" ";
	++it;
}

 其实这个类里面就是一个结点的指针,能够指向结点,在使用C++的运算符重载,就能实现类似++的效果;

1、内嵌类型定义在类里面

2、像指针一样,所指像指针一样:解引用和++/--(像指针一样的行为)

 ❓为什么迭代器不能用<,其实vector和string可以使<,但是list迭代器不能使用小于?

                我们说了迭代器底层不是一个连续空间,虽然我们使用的++,--但是并不是表面上的样子;

这其实就是迭代器的价值,封装底层实现 ,让我们忽视底层细节,我们在使用其他STL库时,统一的访问形式降低了使用成本,迭代器默默承受了一切,但是当你实现迭代器你就发现,那有什么岁月静好,只是有人替你负重前行。

但是,我们学习要关注底层;接下来,让我们走进list迭代器的模拟实现!! 

template <class T>
	class __list_iterator//迭代器
	{
		typedef list_node<T> node;
		node* _pnode;
	public:
		__list_iterator(node* p)
			:_pnode(p)
		{}
		T& operator*()
		{
			return _pnode->_val;
		}
		bool operator != (__list_iterator<T>& it)//参数一定加引用
		{
			return it._pnode != _pnode;
		}
		void operator++()//这里实现有些问题其实是应该携带返回值的,返回*this;
		{
			_pnode =_pnode->_next;
		}
	};

❓但是还是存在一些问题,const迭代器呢?

(const迭代器和普通迭代器的区别就是不可通过*it去修改值。)

如果只是如下这样是解决不了问题的;

	// ++cit 这样的话,可以解引用,但是不能++
		/*const T& operator*() const
		{
		return _pnode->_data;
		}*/

    template <class T>
	class __list_const_iterator
	{
		typedef list_node<T> node;
		node* _pnode;
	public:
		__list_const_iterator(node* p)
			:_pnode(p)
		{}
		const T& operator*()
		{
			return _pnode->_val;
		}
		bool operator != (__list_const_iterator& it)//参数一定加引用
		{
			return it._pnode != _pnode;
		}
		void operator++()//这里实现有些问题其实是应该携带返回值的,返回*this;
		{
			_pnode = _pnode->_next;
		}
	};

 我们单独实现了const_iterator的一个类但是,大佬之所以为大佬,就是代码的复用,能写出来常人难以形容的代码;这里简单复现一下:

template <class T,class Ref>
	class __list_iterator//迭代器
	{
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref> Self;
		node* _pnode;
	public:
		__list_iterator(node* p)
			:_pnode(p)
		{}
		/*T& operator*()//我这里自己写的就很迷,
		{
			return _pnode->_val;
		}*/
		Ref& operator*()//返回值不同不构成重载而且不是const迭代器这里传回的返回值也不是const
		{
			return _pnode->_val;
		}
		bool operator != (const Self& it)//参数一定加引用
		{
			return it._pnode != _pnode;
		}
		Self&  operator++()
		{
			_pnode =_pnode->_next;
			return *this;
		}
		Self& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}
	};

增删查改 

insert在list position 位置中插入值为val的元素
iterator insert(iterator pos, const T& val)
		{
			node* NewNode = new node(val);
			node* cur = pos._pnode;
			node* pre = cur->_prev;

			pre->_next = NewNode;
			NewNode->_prev = pre;
			NewNode->_next = cur;
			cur->_prev = NewNode;
			return iterator(pos);
		}
erase删除list position位置的元素
iterator erase(iterator pos)
		{
			assert(pos != end());

			node* prev = pos._pnode->_prev;
			node* next = pos._pnode->_next;

			prev->_next = next;
			next->_prev = prev;

			delete pos._pnode;

			return iterator(next);
		}
push_front在list首元素前插入值为val的元素
pop_front删除list中第一个元素
push_back在list尾部插入值为val的元素
pop_back删除list中最后一个元素
        void push_back(T val)
		{
			//新建结点
		/*	node* NewNode = new node(val);
			node* tail = _head->_prev;
			tail->_next = NewNode;
			NewNode->_prev = tail;
			NewNode->_next = _head;
			_head->_prev = NewNode;*/
			insert(end(), val);
		}
		void pop_front()
		{
			erase(begin());
		}

		void pop_back()
		{
			erase(--end());
		}

 我们看到大多数代码均是复用出来的,所以代码复用真的很好,要学会“偷懒”。还是那句话

❗️代码复用真的好好 

迭代器失效问题

在vector中,insert和erase是失效的,但是在list迭代器中insert迭代器不失效;erase迭代器必然失效,空间结点都没了;

所以在erase后注意迭代器的使用;

拷贝构造

在拷贝构造前,先实现下析构函数,至于为什么,请看下文,实现拷贝构造去准备工作;

clear清空list中的有效元素
    void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

其实这里就是我们要为什么先实现迭代器,实现了迭代器基本的模拟实现就完成了一半,还有一半,我认为是拷贝构造;

 析构函数

        ~list()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

 ❓如果我们不写拷贝构造,那么默认的拷贝构造是否能用? 

 

报错了,如果把析构删掉,这段程序就通过了,所以要完成析构我们才能够正确的认识,这个默认的拷贝构造是一个浅拷贝,那就是同一块空间析构两次,大问题呀!

 这里实现一个比较牛的写法,第一次见,我看不懂,但大受震撼!

        void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
		}

		list(const list<T>& it)
		{
			empty_initialize();//这里如果不单独开辟一个空间就会出现问题
			list<T> tmp(it.begin(), it.end());//迭代器构造函数
			swap(tmp);
		}

 ❗️注意:empty_initialize();这个函数就是开辟一个头结点,这里如果不开头结点,交换给临时变量list,在析构时会默认存在头结点,所以会调用clear(),这时候对空访问会出才能问题。

 赋值运算符重载

	    list<T>& operator=(list<T> lt)
		//list& operator=(list lt) // 不建议
		{
			swap(lt);
			return *this;
		}

这真的很无敌,可能拷贝构造体现不出来,但是赋值运算符重载很无敌;

这里参数必须是传参而不是引用,直接调用拷贝构造;直接换;

size

size返回list中有效节点的个数

 size这个是比较麻烦的,如果你使用范围for进行遍历,一次可能效率还好,但是频繁size效率是让人难以忍受的;以空间换时间的方法,增加4个字节存储size;

这里还有一个问题,我们之前已经写了很多代码,如果加会不会很麻烦,实际上代码复用的好处就出现了,直接在复用的地方加就好;

有关类名和类型的问题

在类模板中,例如:list<T>,list为类名,list<T>为类型,构造函数名为list而不是list<T>;参数都是lsit<T>但是C++中有些坑,虽然C++中坑太多了

 ❓诶?诶?怎么,又要拿出那个图片了 

  在类中,类名就是类型,所以这样也没问题,但是不建议这么使用,我们只要知道有这个样子的事情就好了;

operator->

struct P
{
	int _row;
	int _col;

	P(int row = 0, int col = 0)
		:_row(row)
		, _col(col)
	{}
};

 如果说我们想打印一个结构化数据,会出现一些问题,

最好的解决方法是重载-> 

很怪很怪

        T* operator->()
		{
			return &_pnode->_val;
		}

 其实就是it.operator->()返回结点数据的地址,就是一个p*,但是p*怎么访问,其实就是一个it->->_row就是it.operator()->_row,但是写两个箭头,很让人无语,所以编译器为了可读性,给我们做了特殊处理。

 ❓这里又又有问题,如果是const呢?

 

struct P
{
	int _row;
	int _col;

	P(int row = 0, int col = 0)
		:_row(row)
		, _col(col)
	{}
};
void print_list(const my::list<P>& lt)
{
	my::list<P>::const_iterator it = lt.begin();
	while (it != lt.end())
	{
        it->_col++;
		cout << it->_col << ":" << it->_col << endl;
		++it;
	}
	cout << endl;
}
int main()
{
	my::list<P> lt;
	P p1(1, 1);
	lt.push_back(p1);
	lt.push_back(p1);
	lt.push_back(p1);
	lt.push_back(P(2, 2));
	lt.push_back(P(3, 3));
	my::list<P>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << it->_col << ":" << it->_col << endl;

		//cout << (*it)._col <<":"<< (*it)._col << "";
		++it;
	}
	cout << endl;
	print_list(lt);
}

这个迭代器是有问题的呀,发现居然是能修改的;

所以可以再次修改迭代器的代码:

        typedef __list_iterator<T,T&,T*> iterator;
		typedef __list_iterator<T, const T&,const T*> const_iterator;
template <class T, class Ref, class Ptr>
	class __list_iterator//迭代器
	{
	public:
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref, Ptr> Self;
		node* _pnode;

		__list_iterator(node* p)
			:_pnode(p)
		{}
		Ptr operator->()
		{
			return &_pnode->_val;
		}
		Ref& operator*()
		{
			return _pnode->_val;
		}
		bool operator != (const Self& it)
		{
			return it._pnode != _pnode;
		}
		
		Self&  operator++()
		{
			_pnode =_pnode->_next;
			return *this;
		}
		Self& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}
	};

无敌, 类型typedef让我们修改很无敌,方便我们修改;

list和vector

vectorlist
底 层 结 构动态顺序表,一段连续空间带头结点的双向循环链表
随 机 访 问支持随机访问,访问某个元素效率O(1)不支持随机访问,访问某个元素
效率O(N)
插 入 和 删 除任意位置插入和删除效率低,需要移动元素,时间复杂
度为O(N),插入时有可能需要增容,增容:开辟新空
间,拷贝元素,释放旧空间,导致效率更低
任意位置插入和删除效率高,不
需要搬移元素,时间复杂度为
O(1)
空 间 利 用 率底层为连续空间,不容易造成内存碎片,空间利用率
高,缓存利用率高
底层节点动态开辟,小节点容易
造成内存碎片,空间利用率低,
缓存利用率低
迭 代 器原生指针对原生态指针(节点指针)进行封装
迭 代 器 失 效在插入元素时,要给所有的迭代器重新赋值,因为插入
元素有可能会导致重新扩容,致使原来迭代器失效,删
除时,当前迭代器需要重新赋值否则会失效
插入元素不会导致迭代器失效,
删除元素时,只会导致当前迭代
器失效,其他迭代器不受影响
使 用 场 景需要高效存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随
机访问

总结

好了List告一段落,所有细节都是难以,只有亲自实现才能够注意到;把握好不同容器的使用时机,才能够方便我们的使用。下期再见!!

 

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

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

相关文章

three.js中Meshline库的使用

three.js中Meshline的使用 库的地址为什么要使用MeshLine,three.js内置的线不好用吗?MeshLine入门MeshLine的深入思考样条曲线一个问题 库的地址 https://github.com/spite/THREE.MeshLine?tabreadme-ov-file 为什么要使用MeshLine,three.js内置的线不好用吗? 确实不好用,…

python222网站实战(SpringBoot+SpringSecurity+MybatisPlus+thymeleaf+layui)-Tag标签管理实现

锋哥原创的SpringbootLayui python222网站实战&#xff1a; python222网站实战课程视频教程&#xff08;SpringBootPython爬虫实战&#xff09; ( 火爆连载更新中... )_哔哩哔哩_bilibilipython222网站实战课程视频教程&#xff08;SpringBootPython爬虫实战&#xff09; ( 火…

2023年CSDN年底总结-独立开源创作者第一年

2023年最大的变化&#xff0c;就是出来创业&#xff0c;当独立开源创作者&#xff0c;这一年发起SolidUI开源项目&#xff0c;把知乎重新开始运营起来。CSDN粉丝破万&#xff0c;CSDN博客专家和AI领域创作者。 2023年年度关键词&#xff1a;创业 https://github.com/CloudOrc…

HCIP:三层架构

配置r1 配置r2 给sw1在vlanif1上配地址&#xff0c;并且ping 23.1.1.1 可通 给sw2配地址 在sw1上做汇总 在sw2上做汇总 在sw1上启动 在sw2上启动 在sw1上启动vlan 在sw2上启动vlan 在sw1上给接口配置vlan 在sw2上给接口配置vlan 在sw3上划分vlan 在sw4上划分vlan 给sw5划分vlan…

小红书如何做混部?

作者&#xff1a;宋泽辉&#xff08;小红书&#xff09;、张佐玮&#xff08;阿里云&#xff09; 编者按&#xff1a; Koordinator 是一个开源项目&#xff0c;是基于阿里巴巴内部多年容器调度、混部实践经验孵化诞生&#xff0c;是行业首个生产可用、面向大规模场景的开源混…

【RabbitMQ】延迟队列之死信交换机

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《RabbitMQ实战》。&#x1f3af;&#x1f3af; &am…

10.多柱状图(MuliBarChart)

愿你出走半生,归来仍是少年&#xff01; 环境&#xff1a;.NET 7、MAUI 话接上回&#xff08;9.单柱状图&#xff08;SingleBarChart&#xff09;&#xff09;&#xff0c;从单柱拓展到多柱状图。 1.数据设置 private void InitValue(List<BasicSerieDto> dtos){Serie…

[ACM学习] 进制转换

进制的本质 本质是每一位的数位上的数字乘上这一位的权重 将任意进制转换为十进制 原来还很疑惑为什么从高位开始&#xff0c;原来从高位开始的&#xff0c;可以被滚动地乘很多遍。 将十进制转换为任意进制

泽众云真机-机型集中化运维方案升级全面完成

2024年元月份&#xff0c;泽众云真机运维团队&#xff0c;经过几个月软硬件多轮安装调试&#xff0c;机型集中化运维方案升级全面完成。解决了云真机的机型集中化运维难题&#xff0c;方便了运营人员手机管理。 具体如下&#xff1a; 1、集中化运维&#xff0c;如服务器、PC、…

数学建模学习笔记||一文了解美赛论文如何写作

目录 ​编辑 Title/标题 要求 形式 Summary Sheet/摘要 要求 三要素 书写特点 内容 开头段 中间段 格式 内容 结尾段 关键词 Contents/目录 Introduction/引言 Problem Background/问题背景 Restatement of the Problem/问题重述 Literature Review/文献综…

RabbitMQ 笔记一

概览&#xff1a; MQ基本概念 RabbitMQ入门 基本工作模 1.MQ是什么&#xff1f; MQ:Message Queue, 存储消息的中间件&#xff0c;是消息发送过程中的暂存容器&#xff0c;主要用于解决分布式系统进程间的通信。 分布式系统通信的两种方式&#xff1a;直接远程调用、借助第三…

vue---打印本地当前时间Demo

<template><view class"content" tap"getCurrentTime()">打印时间</view> </template><script>export default {data() {return {title: Hello}},onLoad() {},methods: {getCurrentTime() {//获取当前时间并打印var _this …

E7数据库备份和恢复

E7数据库备份和恢复 一、实验目的 在Mysql上&#xff0c;学习如何备份数据库和恢复的各种方法。 二、实验要求: 1、基本硬件配置:英特尔Pentium III 以上,大于4G内存&#xff1b; 2、软件要求:Mysql&#xff1b; 3、时间:1小时&#xff1b; 4、撰写实验报告并按时提交。 三、…

windows下git pull超时,ping不通github

报错 ssh: connect to host github.com port 22: Connection timed out fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 解决办法 修改hosts 最后加一行&#xff0c;文件位置&#xff1a;…

利用 “diart“ 和 OpenAI 的 Whisper 简化实时转录

利用 "diart" 和 OpenAI 的 Whisper 简化实时转录 工作原理 Diart 是一个基于人工智能的 Python 库&#xff0c;用于实时记录说话者语言&#xff08;即 "谁在什么时候说话"&#xff09;&#xff0c;它建立在 pyannote.audio 模型之上&#xff0c;专为实时…

Ubuntu 22.04安装Nginx负载均衡

君衍. 一、编译安装Nginx二、轮询算法实现负载均衡三、加权轮询算法实现负载均衡四、ip_hash实现负载均衡 一、编译安装Nginx 这里我们先将环境准备好&#xff0c;我使用的是Ubuntu22.04操作系统&#xff1a; 这个是我刚安装好的&#xff0c;所以首先我们进行保存快照防止安装…

金融OCR领域实习日志(二)——四种OCR模型效果测试(附图)

文章目录 四种模型ocr效果简单测试模型场景1.paddle框架下PP-OCRv31.1.效果如下&#xff1a;1.2.总结 2.paddle框架下ppocr_server_v22.1.效果如下2.2.总结 3.CnOCR3.1.效果如下3.2.总结 4.TesseractOCR4.1.效果如下4.2.总结 5.后续想法 四种模型ocr效果简单测试 模型 PP-OCR…

文件操作---C++

文件操作目录 1.文本文件1.1写文件1.2读文件1.2.1第一种方式&#xff1a;流输入方式1.2.2第二种方式&#xff1a;getline成员函数1.2.3第三种方式&#xff1a;getline全局函数1.2.4第四种方式&#xff1a;按一个一个字符读取 2.二进制文件2.1写文件2.2读文件 程序运行时产生的数…

【C++进阶】STL容器--list使用迭代器问题分析

目录 前言 1. list的基本使用 1.1 list构造函数 1.2 list迭代器 1.3 list capacity 1.4 list元素访问 1.5 list 修改操作 insert erase swap resize clear 2. list失效迭代器问题 3. list使用算法库函数问题 总结 前言 list&#xff08;链表&#xff09;在C中非常重要…

分享7种SQL的进阶用法

分享7种SQL的进阶用法 前言 还只会使用SQL进行简单的insert、update、detele吗&#xff1f;本文给大家带来7种SQL的进阶用法&#xff0c;让大家在平常工作中使用SQL简化复杂的代码逻辑。 1.自定义排序&#xff08;ORDER BY FIELD&#xff09; 在MySQL中ORDER BY排序除了可以…