C++初阶:list的使用和模拟实现

news2024/11/15 19:55:31

关于list可以先看一下这个文档:list文档

一.list的介绍和使用

1.1 list的介绍

list实际上就是链表,是带头双向循环链表。

1.2 list的使用

list的使用跟我们以前用C语言实现时的一样。push,pop,insert等等。

1.2.1list的构造

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<int> l1;                         // 构造空的l1
list<int> l2(4, 100);                 // l2中放4个值为100的元素
list<int> l3(l2.begin(), l2.end());  // 用l2的[begin(), end())左闭右开的区间构造l3
list<int> l4(l3);                    // 用l3拷贝构造l4

上面只是C++98的版本,在C++11中也支持这样构造:

list<int> l5 = { 1,2,3,4,5 };

 1.2.2迭代器的使用

因为是带头循环双向链表,所以begin是第一个元素,end是最后一个元素的下一个节点(哨兵位节点)。而rbegin和rend就刚好是它们两个的相反位置。

//这里用数组的元素来初始化l
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array + sizeof(array) / sizeof(array[0]));
// 使用正向迭代器正向list中的元素
// list<int>::iterator it = l.begin();   // C++98中语法
auto it = l.begin();                     // C++11之后推荐写法
while (it != l.end())
{
	cout << *it << " ";
	++it;
}
cout << endl;

// 使用反向迭代器逆向打印list中的元素
// list<int>::reverse_iterator rit = l.rbegin();
auto rit = l.rbegin();
while (rit != l.rend())
{
	cout << *rit << " ";
	++rit;
}
cout << endl;
return 0;

打印出来的实际上就是正向和逆向。

1.2.3 capacity

关于capacity的重要的就两个

empty判空,不为空返回true,为空返回false
size返回有效节点个数
	list<int> l;
	if (l.empty())
	{
		cout << "l为空" << endl;
	}
	l = { 1,2,3,4,5 };
	cout << l.size() << endl;

1.2.4 access

front返回list中第一个节点值的引用
back

返回list中最后一个节点值的引用

1.2.5modify

push_back

尾插

pop_back尾删
push_front头插
pop_back头删
insert在list position 位置中插入值为val的元素
erase删除list position位置的元素
swap交换两个list中的元素
clear清空list中的有效元素
void PrintList(const list<int>& l)
{
	for (auto& it : l)
	{
		cout << it << " ";
	}
	cout << endl;
}
int main()
{

	int array[] = { 1, 2, 3 };
	list<int> L(array, array + sizeof(array) / sizeof(array[0]));

	// 在list的尾部插入4,头部插入0
	L.push_back(4);
	L.push_front(0);
	PrintList(L);//0 1 2 3 4

	// 删除list尾部节点和头部节点
	L.pop_back();
	L.pop_front();
	PrintList(L);//1 2 3

	// 获取链表中第二个节点
	auto pos = ++L.begin();
	cout << *pos << endl;//2

	// 在pos前插入值为4的元素
	L.insert(pos, 4);
	PrintList(L);// 1 4 2 3

	// 在pos前插入5个值为5的元素
	L.insert(pos, 5, 5);
	PrintList(L);//1 4 5 5 5 5 5 2 3

	// 在pos前插入[v.begin(), v.end)区间中的元素
	vector<int> v{ 7, 8, 9 };
	L.insert(pos, v.begin(), v.end());
	PrintList(L);//1 4 5 5 5 5 5 7 8 9 2 3

	// 删除pos位置上的元素
	L.erase(pos);
	PrintList(L);//1 4 5 5 5 5 5 7 8 9 3

	// 删除list中[begin, end)区间中的元素,即删除list中的所有元素
	L.erase(L.begin(), L.end());
	PrintList(L);//  没有元素不再打印


	list<int> l2 = { 1,2,3 };
	L.swap(l2);
	PrintList(L);//1 2 3
	PrintList(l2);//   没有元素不打印

	// 将l2中的元素清空
	l2.clear();
	cout << l2.size() << endl;//0
	return 0;
}

 1.2.6迭代器失效

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

下面就是一个典型的迭代器失效问题:

int main()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list<int> l(array, array + sizeof(array) / sizeof(array[0]));
	auto it = l.begin();
	while (it != l.end())
	{
		// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
		l.erase(it);
		++it;
	}

	return 0;
}

要想解决上面的问题,只就要更新一下it就行了.

int main()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list<int> l(array, array + sizeof(array) / sizeof(array[0]));
	auto it = l.begin();
	while (it != l.end())
	{
		// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
		it = l.erase(it);
		//++it;
	}

	return 0;
}

二.list的模拟实现

关于list的模拟实现因为结构与前面的string和vector完全不同,所以在进行模拟实现的时候要注意它们之间的差异,最主要和最难的一点就是它的迭代器如何实现实现。因为我们要对它进行封装用一个范围for就可以实行这两种结构的遍历。

2.1迭代器的实现

它的结构在空间上不是连续的,所以我们不能直接以原生指针进行封装。

先把节点的结构写出来:

	template <class T>
	struct ListNode
	{
		ListNode(const T& val = T())
			: _prev(nullptr)
			, _next(nullptr)
			, _val(val)
		{}

		ListNode<T>* _prev;//指向前一个节点
		ListNode<T>* _next;//指向后一个节点
		T _val;
	};

因为节点的地址都不是连续的,所以我们需要重载关于节点的++,--等内容。比如++就要重载为跳到下一个节点。这一点与顺序表是不同的。

	template<class T,class Ref,class Ptr>
	class ListIterator
	{
	public:
		typedef ListNode<T> Node;
		typedef ListIterator<T,Ref,Ptr> Self;
	public:
		//这里的重定义是为了实现反向迭代器
		typedef Ref Ref;
		typedef Ptr Ptr;
		Node* _node;
		//构造函数
		ListIterator(Node* node=nullptr)
			:_node(node)
		{}
		Ref operator*()
		{
			return _node->_val;//返回的是数值的引用
		}
		Ptr operator->()
		{
			return &_node->_val;//返回的是地址
		}
		Self& operator++()//前置++
		{
			_node = _node->_next;
			return *this;//this指针指向的_node移动
		}
		Self operator++(int)//后置++
		{
			Self tmp(*this);//构造函数
			_node = _node->_next;
			return tmp;
		}
		Self& operator--()//前置--
		{
			_node = _node->_prev;
			return *this;
		}

		Self operator--(int)//后置--
		{
			Self temp(*this);
			_node = _node->_prev;
			return temp;
		}
		bool operator!=(const Self& l) const
		{
			return _node != l._node;
		}
		bool operator==(const Self& l) const
		{
			return _node == l._node;
		}
	};

注意: 

在ListIterator模板类中,Ref表示引用的类型,Ptr表示指针的类型。这两个类型参数用于定义迭代器返回值的类型。

在operator*()函数中,返回的是_node节点的值的引用,即Ref类型的值。

在operator->()函数中,返回的是_node节点的值的地址,即Ptr类型的地址。

然后就是反向迭代器:

	template<class Iterator>
	class ReverseListIterator
	{
		// 注意:此处typename的作用是明确告诉编译器,Ref是Iterator类中的一个类型,而不是静态成员变量
		// 否则编译器编译时就不知道Ref是Iterator中的类型还是静态成员变量
		// 因为静态成员变量也是按照 类名::静态成员变量名 的方式访问的
	public:
		typedef typename Iterator::Ref Ref;
		typedef typename Iterator::Ptr Ptr;
		typedef ReverseListIterator<Iterator> Self;//类型是正向迭代器类型
		Iterator _it;//用正向迭代器创建

		ReverseListIterator(Iterator it)
			:_it(it)//初始化
		{}

		//
		
		Ref operator*()
		{
			Iterator temp(_it);//用_it初始化对象temp
			--temp;//调用正向迭代器的前置--,因为rbegin的位置在哨兵位,没有数据,但我们在外面使用时认为是有数据的
			return *temp;//调用正向迭代器的operator*()
		}

		Ptr operator->()
		{
			return &(operator*());//与上面同理,需要调整一下位置,最开始指向哨兵位的上一个节点
		}

		//
		// 迭代器支持移动
		Self& operator++()
		{
			--_it;//因为是反向的,所以这里是--
			return *this;//返回的是自身迭代器的引用
		}
		
		Self operator++(int)
		{
			Self temp(*this);
			--_it;
			return temp;//与上面同理
		}

		Self& operator--()
		{
			++_it;
			return *this;
		}

		Self operator--(int)
		{
			Self temp(*this);
			++_it;
			return temp;
		}

		//
		// 迭代器支持比较
		bool operator!=(const Self& l)const
		{
			return _it != l._it;
		}

		bool operator==(const Self& l)const
		{
			return _it != l._it;
		}

	};

对于反向迭代器需要注意的一点就是反向迭代器是在正向迭代器的基础上实现的,里面用到的--和++等都是用的正向迭代器的。然后就是因为这是双向循环带头链表,关于最后一个位置的处理要注意一下。

最后关于迭代器的就是迭代器的正式实现:
 

//迭代器实现
iterator begin()
{
	iterator it(_head->_next);
	return it;
	//return iterator(_head->_next);//用的都是匿名对象
}
iterator end()
{
	return iterator(_head);
}

const_iterator begin()const
{
	return const_iterator(_head->_next);
}

const_iterator end()const
{
	return const_iterator(_head);
}

reverse_iterator rbegin()
{
	return reverse_iterator(end());
}

reverse_iterator rend()
{
	return reverse_iterator(begin());
}

const_reverse_iterator rbegin()const
{
	return const_reverse_iterator(end());
}

const_reverse_iterator rend()const
{
	return const_reverse_iterator(begin());
}

到这里才算是真正完成了迭代器。

 2.2模拟实现list的准备

为了更清晰更清晰的实现list,我们需要清晰的知道各个名字都代表什么。还有里面的成员变量。

template<class T>
class list
{
public:
	typedef ListNode<T> Node;
	typedef ListIterator<T, T&, T*> iterator;
	typedef ListIterator<T, const T&, const T*> const_iterator;

	typedef ReverseListIterator<iterator> reverse_iterator;
	typedef ReverseListIterator<const_iterator> const_reverse_iterator;
private:
	void CreateHead()
	{
		_head = new Node;
		_head->_prev = _head;
		_head->_next = _head;
	}
	Node* _head;
};

2.3modify

这里主要是对数据的插入删除,insert  erase  等等。

// 在pos位置前插入值为val的节点
iterator insert(iterator pos, const T& val)
{
	Node* pNewNode = new Node(val);
	Node* pCur = pos._node;
	// 先将新节点插入
	pNewNode->_prev = pCur->_prev;
	pNewNode->_next = pCur;
	pNewNode->_prev->_next = pNewNode;
	pCur->_prev = pNewNode;
	return iterator(pNewNode);
}
// 删除pos位置的节点,返回该节点的下一个位置
iterator erase(iterator pos)
{
	// 找到待删除的节点
	Node* pDel = pos._node;
	Node* pRet = pDel->_next;

	// 将该节点从链表中拆下来并删除
	pDel->_prev->_next = pDel->_next;
	pDel->_next->_prev = pDel->_prev;
	delete pDel;

	return iterator(pRet);
}
void clear()
{
	Node* cur = _head->_next;

	// 采用头删除删除
	while (cur != _head)
	{
		_head->_next = cur->_next;
		delete cur;
		cur = _head->_next;
	}

	_head->_next = _head->_prev = _head;
}
void resize(size_t newsize, const T& data = T())
{
	size_t oldsize = size();
	if (newsize <= oldsize)
	{
		// 有效元素个数减少到newsize
		while (newsize < oldsize)
		{
			pop_back();
			oldsize--;
		}
	}
	else
	{
		while (oldsize < newsize)
		{
			push_back(data);
			oldsize++;
		}
	}
}
void swap(list<T>& l)
{
	std::swap(_head, l._head);
}
void push_back(const T& val)
{
	insert(end(), val);
}

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

void push_front(const T& val)
{
	insert(begin(), val);
}

void pop_front()
{
	erase(begin());
}	

2.4capacity

bool empty()const
{
	return _head->_next == _head;
}
// List的容量相关
size_t size()const
{
	Node* cur = _head->_next;
	size_t count = 0;
	while (cur != _head)
	{
		count++;
		cur = cur->_next;
	}

	return count;
}

2.5access

链表不支持[]。

T& front()
{
	return _head->_next->_val;
}

const T& front()const
{
	return _head->_next->_val;
}

T& back()
{
	return _head->_prev->_val;
}

const T& back()const
{
	return _head->_prev->_val;
}

2.6list的构造

因为这里要频繁用到前面的东西,所以就把它放在了最后。

// List的构造
list()
{
	CreateHead();
}

list(int n, const T& value = T())
{
	CreateHead();
	for (int i = 0; i < n; ++i)
		push_back(value);
}

template <class Iterator>
list(Iterator first, Iterator last)
{
	CreateHead();
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

list(const list<T>& l)
{
	CreateHead();

	// 用l中的元素构造临时的temp,然后与当前对象交换
	list<T> temp(l.begin(), l.end());
	this->swap(temp);
}

list<T>& operator=(list<T> l)
{
	this->swap(l);
	return *this;
}

~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

到这里关于list的就分享完了,如有错误还请多多指出。

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

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

相关文章

【从零开始一步步学习VSOA开发】开发环境搭建

开发环境搭建 开发 VSOA 首先需要搭建开发环境&#xff0c;这里讲解 Windows 下 C/C 开发环境搭建方法。 下载 IDE 并申请授权码 SylixOS 的开发和部署需要 RealEvo-IDE 的支持&#xff0c;因此您需要先获取 RealEvo-IDE 的安装包和注册码。 RealEvo-IDE 分为体验版和商业版…

简单的 微服务netflix 学习

简单的 微服务netflix 学习 一.Eureka 学习 1. 服务简单搭建 1.1 首先确定pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-…

FPGA入门系列:计数器

目录 微信公众号获取更多FPGA相关源码&#xff0c;且微信公众号为首发&#xff0c;第一时间获取最新文章&#xff1a; 计数器是一种典型的时序器件&#xff0c;常用于对时钟脉冲的个数进行计数&#xff0c;还用于定时、分频、产生同步脉冲等 按触发方式分&#xff1a;同步计数…

国内SSL证书颁发机构哪家服务更优质?

SSL证书作为保障网站数据传输安全的关键工具&#xff0c;其重要性不言而喻。选择一个可靠的SSL证书代理商&#xff0c;不仅能够提供多样化的证书类型&#xff0c;而且能在众多品牌中进行比较&#xff0c;选择最适合自己的、性价比更高的产品。此外&#xff0c;优质的代理商还能…

JAVA项目基于Spring Boot的多媒体素材库

目录 一、前言 二、技术介绍 三、项目实现流程 四、论文流程参考 五、核心代码截图 专注于大学生实战开发、讲解和毕业答疑等辅导&#xff0c;获取源码后台 一、前言 信息化管理时代已至&#xff0c;计算机与互联网技术深度融合于生活点滴。本系统源于对用户需求的深刻洞…

Spark学习之SaprkCore

FlinkCore 1、JavaAPI 1、创建一个Topic并写入数据 向Kafka写数据 如果topic不存在则会自动创建一个副本和分区数都是1的topic package com.shujia.kafka;import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord…

JAVASpring学习Day2

面向切面编程 (AOP) 面向切面编程是一种编程范式&#xff0c;用于在程序中分离关注点&#xff0c;例如日志记录、事务管理和安全性。它主要由以下几个关键组成部分构成&#xff1a; 连接点 (Join Point)&#xff1a;在程序执行过程中可以插入切面的点&#xff0c;通常是方法的…

I2C 设备驱动编写流程

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、修改设备树1、 IO 修改或添加2、在 i2c1 节点追加 ap3216c 子节点3、查看设备树节点创建是否成功 二、AP3216C 驱动编写 前言 提示&#xff1a;这里可以添…

9.11 与 9.9 哪个大? 大模型幻觉从何而来?用最通俗的例子讲清楚大模型原理。

​如下图&#xff0c;我们使用用 gpt-4-turbo 模型为例&#xff0c;问9.11 与 9.9 哪个大&#xff0c;并让他一步一步给出分析步骤。你会发现&#xff0c;它开始了胡说八道&#xff0c;这就是“大模型幻觉” 。 那么问题来了&#xff0c;为什么会出现这种结果&#xff1f;幻觉从…

【Python代码】如何在多个Excel文件中高效找出含有特定关键词的文件?

点击上方"蓝字" 关注木易巷&#xff01; 哈喽&#xff0c;大家好&#xff0c;木易巷来啦&#xff01; 想象一下&#xff0c;如果你有一个文件夹&#xff0c;里面堆满了近百个Excel文件&#xff0c;你需要从中找出包含特定关键词文本的文件。文件格式不统一&#xf…

仓颉语言 -- 宏

使用新版本 &#xff08;2024-07-19 16:10发布的&#xff09; 1、宏的简介 宏可以理解为一种特殊的函数。一般的函数在输入的值上进行计算&#xff0c;然后输出一个新的值&#xff0c;而宏的输入和输出都是程序本身。在输入一段程序&#xff08;或程序片段&#xff0c;例如表达…

【OpenCV C++20 学习笔记】提取水平和垂直线条

提取水平和垂直线条 原理实操——去除五线谱的五线二进制化提取垂直对象完善边缘和最终输出图片黑白反转平滑 完整代码 其他图片元素提取实践提取水平线条提取音符轮廓 原理 在腐蚀和膨胀操作中&#xff0c;通过卷积核(kernel)&#xff0c;或者结构元素(structuring element)&…

vue-router核心TS类型

NavigationFailureType 枚举&#xff1a; export declare enum NavigationFailureType {/*** An aborted navigation is a navigation that failed because a navigation* guard returned false or called next(false)*/aborted 4,/*** A cancelled navigation is a navigati…

arduino程序-MC猜数字5、6(基础知识)

arduino程序-MC猜数字5、6&#xff08;基础知识&#xff09; 1-23 MC猜数字-5 自定义函数自定义函数自定义清理显示内容函数displayClear&#xff08;&#xff09;带参数函数displayNumber带参数、返回值的函数 1-24 MC猜数字-6 完成制作显示0~9数字函数改造产生随机数字函数改…

嵌入式人工智能(42-基于树莓派4B的红外遥控)

1、简介 红外遥控想必对大家来说都不陌生&#xff0c;红外也属于无线通信的一种&#xff0c;只要是无线通信&#xff0c;必然要用电磁波&#xff0c;要理解无线通信的本质和原理&#xff0c;不管用哪个频段都要学习电磁场与电磁波&#xff0c;这是一个难度很大的课&#xff0c…

IT事件经理在数字企业中的角色和责任

什么是IT事件经理&#xff1f; IT事件经理有时也被称为事件指挥官&#xff0c;他们承担着管理组织事件响应的总体责任&#xff0c;从委派各种事件响应任务到与每个利益相关者进行沟通和协调。 示例&#xff1a;当一个全球性的电子商务平台在一次销售活动中流量激增&#xff0c…

George Danezis谈Mysticeti的共识性能

Sui的新共识引擎Mysticeti已经在主网上开始分阶段推出。Mysten Labs联合创始人兼首席科学家George Danezis在采访中&#xff0c;讨论了其低延迟如何通过高性能应用程序提升用户体验。 采访视频&#xff1a; https://youtu.be/WHvtPQUa2Q0 中文译文&#xff1a;延迟和吞吐量对…

LSTM与GNN强强结合!全新架构带来10倍推理速度提升

今天来推荐一个深度学习领域很有创新性的研究方向&#xff1a;LSTM结合GNN。 GNN擅长处理图数据关系和特征&#xff0c;而LSTM擅长处理时间序列数据及长期依赖关系。通过将两者结合&#xff0c;我们可以有效提升时间序列预测的准确性和效率&#xff0c;尤其是在处理空间和时间…

手搓交换排序、归并排序、计数排序

文章目录 交换排序冒泡排序快速排序hoare版本挖坑法lomuto前后指针 非递归快速排序 归并排序实现计数实现排序代码测试排序算法性能 交换排序 冒泡排序 void BubbleSort(int* arr, int n) {for (int i 0; i < n; i){int flag 0;for (int j 0; j < n - i - 1; j){if …

day13 Java基础——逻辑运算符,位运算符及面试题

day13 Java基础——逻辑运算符&#xff0c;位运算符及面试题 1. 逻辑运算符&#xff1a;与&#xff0c;或&#xff0c;非 package operator;public class Demo07 {public static void main(String[] args) {boolean a true;boolean b false;System.out.println("a &…