C++list

news2024/12/23 23:23:39

1. list的介绍及使用

1.1 list的介绍

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

 

1.2 list的使用

因为接口和vector很相似,这里只介绍重点:

1.2.1 list的构造

list 中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展 的能力。以下为list 中一些 常见的重要接口

 1.2.2 list iterator的使用

 

 

注意:
1. begin与end 为正向迭代器,对迭代器 执行++操作,迭代器向后移动
2. rbegin(end)与rend(begin) 为反向迭代器,对 迭代器执行++操作,迭代器向前移动
这是C++中最常用的遍历方式,因为有了迭代器,所有的数据结构都可以用这种方式进行遍历,这体现了C++的封装性。注意这里的iterator不是一个原生指针,而是对其进行了封装。(后面模拟实现再来感受)

 1.2.3 list capacity

 1.2.4 list element access

 1.2.5 list modififiers

这里要提一下的是swap函数的使用问题:
这里list专门提供了一个swap函数用于交换,但是这个函数实际上并没有什么用处,因为其效率非常低,甚至不如把list中的数据拷贝到vector中,然后排完再存到list中的效率高,所以这样来看,这个接口基本用处不大,少量的数据的时候才能使用。

下面看测测swap的性能如何;

void test_op()
{
	srand(time(0));
	const int N = 1000000;
	vector<int> v;
	v.reserve(N);

	list<int> lt1;
	list<int> lt2;
	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		v.push_back(e);
		//lt1.push_back(e);
		lt2.push_back(e);
	}

	 拷贝到vector排序,排完以后再拷贝回来
	int begin1 = clock();
	//for (auto e : lt1)
	//{
	//	v.push_back(e);
	//}
	sort(v.begin(), v.end());
	//size_t i = 0;
	//for (auto& e : lt1)
	//{
	//	e = v[i++];
	//}
	int end1 = clock();
	int begin2 = clock();
	// sort(lt.begin(), lt.end());
	lt2.sort();
	int end2 = clock();

	printf("vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}

vector和list排序的速度对比:

 可以看到vector比list快了2倍。

下面看看把list数据放到vector中排序然后再放回list中的对比:

 对比以上情景,确实证明了list排序不是一个良好的排序,所以list里面的sort要慎用。

当数据到了一千万时,这个差距更加大:

1.2.6 list的迭代器失效

迭代器失效即迭代器所指向的节点的无效,即该节 点被删除了 。因为 list 的底层结构为带头结点的双向循环链表 ,因此 list 中进行插入时是不会导致 list 的迭代 器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响 。(这是基于list的非连续地址的原因)

 正确的写法:

因为这里erase删除后会返回下个元素的迭代器,所有不需要++。

 2. list的模拟实现(重难点)

在模拟实现之前,我们要清楚我们要使用3个类:一个是节点类,一个迭代器类,一个list类。这里为什么要使用迭代器类呢?很重要的一个原因在于迭代器如果使用原生指针的话,就不适合list的空间结构了,原生指针++就是4个字节,显然list类的节点++的结果是不确定的。我们要使用每个节点中存储的下一个节点的地址来进行++。如果我们像C语言中实现双向带头循环链表一样就失去了封装性,还有stl中使用迭代器的方式就是为了让每一种容器都能使用迭代器的方式进行遍历。

下面我们来看看这三个类:

节点类:

//节点类
	template<class T>
	struct ListNode
	{
		ListNode(const T& x)
			:_val(x)
			, _pre(nullptr)
			, _next(nullptr)
		{}
		ListNode<T>*  _pre;
		ListNode<T>*  _next;
		T _val;
	};

节点类比较简单就是两个地址,一个数据。另外提供构造函数进行初始化。

迭代器类:(难点):

首先要明确迭代器类的作用,就是在list类中进行迭代器的++或者其他操作时我们可以将其转换为一个函数调用。  这就是运算符重载的作用,实现了封装性。

还有一个很大的问题就是在实现const迭代器和普通迭代器的时候:

 我们就会发现一个问题:就是我们怎么样能让普通的迭代器调用普通的迭代器的运算符重载,而const迭代器调用const迭代器的运算符重载呢?用正常的方式不可能在同一类里面实现,因为函数重载的类型是不包括参数不同的。如果像下面的做法确实是可以做到的:

template<class T>
	struct __list_iterator
	{
		typedef list_node<T> node;
		node* _pnode;

		__list_iterator(node* p)
			:_pnode(p)
		{}

		// iterator it
		// *it
		// ++it;
		T& operator*()
		{
			return _pnode->_data;
		}

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

		__list_iterator<T>& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}

		__list_iterator<T>& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}

		bool operator!=(const __list_iterator<T>& it)
		{
			return _pnode != it._pnode;
		}
	};

	// 跟普通迭代器的区别:遍历,不能用*it修改数据
	template<class T>
	struct __list_const_iterator
	{
		typedef list_node<T> node;
		node* _pnode;

		__list_const_iterator(node* p)
			:_pnode(p)
		{}

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

		__list_const_iterator<T>& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}

		__list_const_iterator<T>& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}

		bool operator!=(const __list_const_iterator<T>& it)
		{
			return _pnode != it._pnode;
		}
	};

但是不觉得这样写代码冗余吗?

为此设计stl的大佬们就使用了多个模板参数来解决这个问题

 

//迭代器类
	template <class T, class Ref , class Ptr>
	class List_iterator
	{
		typedef List_iterator<T,Ref,Ptr> Self;
		typedef ListNode<T> node;
	public:
		List_iterator(node* _pnode)
			:_Node(_pnode)
		{}
		Ptr operator->()
		{
			return &(_Node->val);
		}
		Ref 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->_pre;
			return *this;
		}
		Self& operator--(int)
		{
			Self tmp(*this);
			_Node = _Node->_pre;
			return tmp;
		}
		bool operator!=(const Self& it)
		{
			return _Node != it._Node;
		}
		bool operator==(const Self& it)
		{
			return _Node == it._Node;
		}
		node* _Node;
	};

这样const迭代器的问题就能很好的解决了。

下面我们看看list类:

list类还是比较简单的,和C语言实现的双向循环带头链表的方式没有很大的差异

//list类
	template<class T>
	class list
	{
	public:
		typedef List_iterator<T,T& ,T*> iterator;
		typedef List_iterator<T ,const T& ,const T*> const_iterator;
		typedef ListNode<T> node;
	public:
		//构造
		list()
		{
			CreateHead();
			_sz = 0;
		}
		传统写法
		//list(const list<T>& l)
		//{
		//	CreateHead();
		//	for (const auto& e : l)
		//	{
		//		push_back(e);
		//	}
		//}
		//现代写法
		list(const list<T>& l)
		{
			CreateHead();
			//使用了另一个对象然后交换他们即可
			list<T> tmp(l.begin(), l.end());
			swap(tmp);
		}
		template <class Iterator>
		list(Iterator first, Iterator last)
		{
			CreateHead();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		//现代写法
		//这里不能使用引用传参,如果使用了就是舍己为人了
		//传过来的对象就和this交换了,就失去了交换的意义
		list<T>& operator=(list<T> l)
		{
		    swap(l);
			return *this;
		}
		void swap(list<T>& l)
		{
			std::swap(_sz ,l._sz);
			std::swap(_head ,l._head);
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		list(int n, const T& value = T())
		{
			CreateHead();
			while (n--)
			{
				push_back(value);
			}
		}
		// List Capacity

		size_t size()const
		{
			return _sz;
		}

		bool empty()const
		{
			return _sz == 0;
		}
		//迭代器
		iterator begin()
		{
			//匿名显得简单
			return iterator(_head->_next);
		}
		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		const_iterator end() const
		{
			return const_iterator(_head);
		}
		//增删查改
		void push_back(const T& val)
		{
			//node* newnode = new node(val);
			//node* tail = _head->_pre;
			 _head         tail   newnode
			//tail->_next = newnode;
			//newnode->_pre = tail;
			//newnode->_next = _head;
			//_head->_pre = newnode;
			insert(end(), val);
		}
		void push_front(const T& val)
		{
			insert(begin(), val);
		}
		void pop_front()
		{
			erase(begin());
		}
		void pop_back()
		{
			erase(--end());
		}

		// List Access

		T& front()
		{
			assert(_sz >= 1);
			return (begin()._Node)->_val;
		}

		const T& front()const
		{
			assert(_sz >= 1);
			return (begin()._Node)->_val;
		}

		T& back()
		{
			assert(_sz >= 1);
			return ((--end())._Node)->_val;
		}

		const T& back()const
		{
			assert(_sz >= 1);
			return ((--end())._Node)->_val;
		}
		//在pos位置之前插入数据
		iterator insert(iterator pos ,const T& val)
		{
			node* newnode = new node(val);
			node* cur = pos._Node;
			node* pre = cur->_pre;
			//链接 newnode pre cur
			newnode->_next = cur;
			newnode->_pre = pre;
			pre->_next = newnode;
			cur->_pre = newnode;
			++_sz;
			return iterator(newnode);
		}
		iterator erase(iterator pos)
		{
			assert(pos != end());
			node* cur = pos._Node;
			node* pre = cur->_pre;
			node* next = cur->_next;
			//pre next
			pre->_next = next;
			next->_pre = pre;
			delete cur;
			return iterator(next);
		}
		void clear()
		{
			//清理不清头节点
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}
	private:
		void CreateHead()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_pre = _head;
		}
		node* _head;
		size_t _sz;
	};

3. list与vector的对比

vector list 都是 STL 中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:

 

vector保证了高效存储,支持随机访问,同时使用vector就不关心插入删除效率
list则相反:大量插入和删除操作,不关心随机访问

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

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

相关文章

外包出去找工作被歧视,投几个简历都说介意外包,不考虑外包。

说起外包这个话题&#xff0c;一直是职场的热门话题。 关于外包&#xff0c;最多被人讨论的就是歧视方面的。比如&#xff0c;关于门禁权限方面的歧视、关于语言交流上的歧视、关于福利上的歧视等等。 01 外包被歧视的情况确实存在 网传有一腾讯外包员工在脉脉上吐槽自己遭受…

python--内置高阶函数、异常处理;模块与包以及python基础部分的总结

文章目录一、内置高阶函数二、异常处理异常处理机制抛出异常三、模块与包python基础部分的总结一、sort()与sorted()函数的区别二、深拷贝和浅拷贝、is与四、基础部分的脑图总括一、内置高阶函数 map()函数 reduce()函数 filter()函数 sorted()函数 #排序 二、异常处理 Indent…

java基础巩固-宇宙第一AiYWM:为了维持生计,测试篇预热【单元测试、性能测试、灰度发布与回滚】~整起

单元测试【就像买保险&#xff0c;希望自己不要用上】是重构的保护网&#xff1a;单元测试可以为重构提供信心&#xff0c;降低重构的成本。我们要像重视生产代码那样&#xff0c;重视单元测试【元测试&#xff08;Unit Testing&#xff09;是针对程序模块&#xff08;软件设计…

协同过滤CF

算法提出 如果让推荐系统领域的从业者选出业界影响力最大、应用最广泛的模型&#xff0c;那么笔者认为90%的从业者会首选协同过滤。1992年, Xerox的研究中心开发了一种基于协同过滤的邮件筛选系统&#xff0c;用以过滤一些用户不感兴趣的无用邮件。2003 年&#xff0c;Amazon …

MySQL数据表的基础知识

目录 一、增 二、查 a、全列查询 b、指定列查询 c、查询字段为表达式 d、别名查询 e、对查询结果进行去重 f、排序 ​g、条件查询 三、改 四、删 以下操作均以student表为基础&#xff1a; 一、增 insert into 表名 values(...); 例如&#xff1a;新增张三同学的…

举个栗子!Tableau 技巧(247):用震波图(Seismogram)查看数据变化

震波图&#xff08;Seismogram&#xff09;是一种像地震波或声波的图表&#xff0c;通常用于表达数据的变化。乍一看&#xff0c;它有点像 蝴蝶图&#xff08;旋风图&#xff09;&#xff0c;数据都分布在轴的零点两侧&#xff0c;但其实两者完全不同。 如下震波图&#xff0c…

Stm32旧版库函数6——ov2640 串口显示图像 串口中断 使用旧版库 模拟IIC

/******************************************************************************* // // 使用单片机STM32F100C8T6 8 // 晶振&#xff1a;8.00M // 编译环境 Keil uVision4 // 在3.3V的供电环境下&#xff0c;就能运行 // 波特率 115200 // 使用&#xff1a;STM32F100C…

SpringCloud Config 分布式配置中心

分步式系统面临配置问题&#xff1a; 微服务意味着要将单体应用中的业务拆分成一个个子服务&#xff0c;每个服务的粒度相对较小&#xff0c;因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行&#xff0c;所以一套集中式的、动态的配置管理设施是必不…

浅谈电气火灾监控系统在煤矿高层公寓中的应用分析

摘要&#xff1a; 煤矿高层公寓做为人员高度密集场所&#xff0c;使用的电器种类繁多&#xff0c;一旦发生电气火灾事故&#xff0c; 其严重性和危害性远高于其它场所。文章通过对煤矿公寓电气线路火灾主要形式的分析&#xff0c;对电气火灾监 控系统在煤矿公寓电气火灾预防及…

NLG解码策略

NLG解码策略 自然语言生成&#xff08;Natural Language Generation&#xff0c;简称NLG&#xff09;&#xff0c;是自然语言处理领域的一个重要分支&#xff0c;在文本摘要生成任务中&#xff0c;另一个重要的分支是自然语言理解&#xff08;Natural Language Understanding&…

传奇GOM引擎版本架设

传奇GOM引擎版本架设 传奇GOM引擎版本我们架设游戏需要用到的工具&#xff1a; 版本&#xff08;游戏类型服务端&#xff09;、DBC2000&#xff08;游戏数据库&#xff09;、RAR&#xff08;解压工具&#xff09;、传奇客户端、服务器、网盘 先把这些架设工具准备好&#xff…

性能测试——

目录 测试的分类 性能测试的指标 性能测试需求分析 ​编辑 性能测试计划及方案 ​编辑​编辑 测试用例设计及执行 梳理系统架构 压力测试报告 测试的分类 性能测试的指标 性能测试需求分析 性能测试计划及方案 测试用例设计及执行 估算系统的qps要了解什么&#xff1a; 系…

Hot100-最小路径和

1 前言 1.1 题目描述 给定一个包含非负整数的 m x n 网格 grid &#xff0c;请找出一条从左上角到右下角的路径&#xff0c;使得路径上的数字总和为最小。 说明&#xff1a;每次只能向下或者向右移动一步。 如下图所示&#xff1a; 输入&#xff1a;grid [[1,3,1],[1,5,1…

ConversionService转换服务使用

前言 在最近分析和写的SpringBoot源码分析(面试官&#xff1a;你说说Springboot的启动过程吧(5万字分析启动过程))中&#xff0c;给自己留了一个关于ConversionService的使用的作业&#xff0c;这不就来补作业了。 使用出处 这个转换服务我这里的例子很简单&#xff0c;就是…

计算机研究生就业方向之去大厂做架构师

我一直跟学生们说你考计算机的研究生之前一定要想好你想干什么&#xff0c;如果你只是转码&#xff0c;那么你不一定要考研&#xff0c;至少以下几个职位研究生是没有啥优势的&#xff1a; 1&#xff0c;软件测试工程师&#xff08;培训一下就行&#xff09; 2&#xff0c;前…

RocketMQ详解及开发用例

概念 Apache RocketMQ作为阿里开源的一款高性能、高吞吐量的分布式消息中间件。 支持Broker和Consumer端消息过滤&#xff0c;支持发布订阅模型和点对点&#xff0c;支持拉pull和推push两种消息模式&#xff0c;单一队列百万消息、亿级消息堆积&#xff0c;支持单master节点&a…

力扣202.快乐数(java语言HashSet方法,类双指针方法)

前言&#xff1a;此题被分类到散列表算法题目中&#xff0c;但乍一看此题实在想不到如何去使用散列表&#xff0c;直到看了官方给的答案。。。。。。 题目描述&#xff1a; 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」 定义为&#xff1a; 对于一个正整数&#…

C++ 基本语法

&#x1f4d2;博客主页&#xff1a; ​​开心档博客主页​​ &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐留言&#x1f4dd; &#x1f4cc;本文由开心档原创&#xff01; &#x1f4c6;51CTO首发时间&#xff1a;&#x1f334;2022年12月12日&#x1f334; ✉…

Python40个自动化办公实战案例,终于实现下班自由啦~

拿来就能用&#xff0c;这么爽的吗&#xff1f;&#xff01; 今天我想聊聊&#xff0c;如何通过Python自动化工具&#xff0c;解决工作中常见的办公效率低下的问题。 你有没有想过&#xff0c;下班晚&#xff0c;加班&#xff0c;可能是因为自己工作比较低效&#xff1f; 回…

wireshark 分析理解DHCP流程

DHCP概念&#xff1a; 动态主机配置协议 DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff09;&#xff0c;是一个应用层协议。该协议允许服务器向客户端动态分配 IP 地址和配置信息。 知识补充&#xff1a; BOOTP&#xff08;Boots…