C++初阶:STL详解(七)——list的模拟实现

news2024/11/18 11:26:11

✨✨小新课堂开课了,欢迎欢迎~✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++:由浅入深篇

小新的主页:编程版小新-CSDN博客

前言:

我们前面已经了解到了list的使用和常见接口了,今天我们来深入底层来看一下list的模拟实。list的模拟实现与前面我们模拟显现的string,vector有所不同,我们需要三个类才能成功模拟实现list,下面我们进入正文。

C++初阶:STL详解(六)——list的介绍和使用-CSDN博客

list各函数接口总览

namespace fu
{
    //节点类的模拟实现
	template<class T>
	struct list_node
	{
		//成员函数
		list_node(const T& val = T()); //构造函数

		//成员变量
		T _val;                 //数据域
		list_node<T>* _next;   //后继指针
		list_node<T>* _prev;   //前驱指针
	};

	//迭代器类的模拟实现
	template<class T, class Ref, class Ptr>
	struct list_iterator
	{
		typedef list_node<T> Node;
		typedef list_iterator<T, Ref, Ptr> self;

		list_iterator(Node* node);  //构造函数

		//各种运算符重载函数
		self operator++();
		self operator--();
		self operator++(int);
		self operator--(int);
		bool operator==(const self& s) const;
		bool operator!=(const self& s) const;
		Ref operator*();
		Ptr operator->();

		//成员变量
		Node* _node; //一个指向结点的指针
	};

	//模拟实现list
	template<class T>
	class list
	{
	public:
		typedef list_node<T> Node;
		typedef list_iterator<T, T&, T*> iterator;
		typedef list_iterator<T, const T&, const T*> const_iterator;

		//默认成员函数
		list();
		list(const list<T>& lt);
		list<T>& operator=(const list<T>& lt);
		~list();

		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;
        
        //容量相关函数
        size_t size() const;
        bool empty() const;

		//访问容器相关函数
		T& front();
		T& back();
		const T& front() const;
		const T& back() const;

		//增删查改
		void insert(iterator pos, const T& x);
		void erase(iterator pos);
		void push_back(const T& x);
		void pop_back();
		void push_front(const T& x);
		void pop_front(); 
		void clear();
		void swap(list<T>& lt);

	private:
		Node* _head; //指向链表头结点的指针
        size_t size;//list节点的个数
	};
}

注意:为了与标准库里的list产生命名冲突,我们在模拟实现的时候需要将其放在自己的命名空间域中。

一.模拟实现节点类

我们知道list其实就是带头双向循环链表

因此我们要模拟实现list,首先就需要模拟实现一个节点类,很形象,这个节点类里要包含三个数据:1.要储存的元素 2.指向前一个节点的前驱指针 3.指向后一个节点的后继指针。 这也就是我们所谓的成员变量。

那这个节点需要的成员函数是什么呢?其实我们只需要一个构造函数即可,这个构造函数的目的就是根据所传数据构造一个节点,至于为什么不再需要析构函数,list的析构函数自己就能胜任释放空间的工作。

1.1构造函数

节点类的构造函数只需根据所给数据构造一个节点,数据域就是所给数据,前驱指针和后继指针我们初始化为空即可。

template<class T>
struct list_node
{
	//成员函数
	list_node(const T& val = T()); //构造函数
	{
		_val = val;
		_next = nullptr;
		_prev = nullptr;
	}

	//成员变量
	T _val;                 //数据域
	list_node<T>* _next;   //后继指针
	list_node<T>*_ prev;   //前驱指针
};

二.模拟实现迭代器类

2.1迭代器类单独模拟的原因

我们在模拟实现string,vector的时候都没有单独模拟实现一个迭代器类,为什么这里到模拟实现list就要如此呢。这就要从二者的区别讲起。

我们知道string和vector都是将数据存储在一个连续的内存空间中,我们通过将指针自增,自减以及解引用就能找到对应位置的数据并对其进行相应的操作,因此此时迭代器的本质也就是指针。

我们再来看list,list其实就是带头双向循环链表,是由一个个节点构造的,而这些节点的地址是随意的,不连续的,我们想通过对指针自增,自减等一些操作来达到我们预期的功能是不行的,指针表示我做不到。

为了解决这一问题,我们只好对节点指针进行封装,并对一些操作符进行重载,使得我们可以像使用string,vector的迭代器一样来使用list的迭代器。这里也就呼应了迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针或者是对指针进行了封装。这样就可以让节点指针看起来和普通指针没有什么区别,但是底层却别有不同

2.2迭代器类的模板参数

迭代器类的模板参数有三个。

template<class T, class Ref, class Ptr>

这里我们还需要定义两个迭代器类型,普通迭代器和const迭代器。

typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

 与模板参数一一对应,Ref就是引用类型,Ptr就是指针类型。

当我们使用普通迭代器时,编译器就会实例化出一个普通迭代器对象,该对象可以读取和修改容器中的元素。

当我们使用const迭代器时,编译器就会实例化出一个const迭代器对象,该对象可以保证不意外修改容器中的元素。

总结:list_iterator 的三个参数分别代表元素类型、引用类型和指针类型。这样的设计提高了类型安全性和灵活性,使得 STL 容器的使用更加方便和安全。

2.3成员函数

1.构造函数

迭代器类实际上就是对结点指针进行了封装,其成员变量就只有一个,那就是结点指针,其构造函数直接根据所给结点指针构造一个迭代器对象即可。

typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> self;

list_iterator(Node* node)
	:_node(node)
{}

2.++运算符重载

首先我们来看前置++,先让结点指针指向后一个结点,然后再返回“自增”后的结点指针即可。

self operator++()
{
	_node = _node->_next;
	return *this;
}

后置++,先记录当前结点指针的指向,然后让结点指针指向后一个结点,最后返回“自增”前的结点指针即可。

self operator++(int)
{
	self tmp (*this);
	_node = _node->next;

	return tmp;
}

3.--运算符重载

首先我们先看前置--,先让节点指针指向前一个节点,然后再返回“自减”后的节点指针即可。

self operator--()
{
	_node = _node->_prev;
	return *this;
}

后置--,先记录点前节点指针的指向,然后让节点指针指向前一个节点,最后返回“自减”前的节点指针即可。

self operator--(int)
{
	self tmp(*this);
	_node = _node->_prev;

	return tmp;

}

4.==运算符重载

判断这两个迭代器的节点指针的指向是否相同即可。

bool operator==(const self& s) const
{
	return _node == s._node;
}

5.!=运算符重载

判断这两个迭代器的节点指针的指向是否不同即可。

bool operator!=(const self& s) const
{
	return _node != s._node;
}

6.*运算符重载

直接返回当前节点指针所指向节点的数据即可。

Ref operator*()
{
	return _node->data;
}

7.->运算符重载

直接返回当前节点指针所指向的节点数据的地址即可。

Ptr operator->()
{
	return &_node->_val;
}

三.list的模拟实现

默认成员函数

构造函数

list是带头双向循环链表,我们在构造时,直接申请一个头结点(哨兵位)让其自己指向自己即可。这里我们也可以多添加一个_size来记录当前节点的个数,方便我们后面的操作。

 

typedef list_node<T> node;
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

list()
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
    _size = 0;//记录当前节点的个数
}

 拷贝构造函数

拷贝构造函数就是根据所给容器拷贝构造出一个对象,对于拷贝构造函数的实现,我们只需先创建一个头结点,让其自己指向自己,遍历原容器一个个尾插即可。

list(const list<T>& It)
{
	_head = new _node;
	_head->_next = _head;
	_head->_prev = _head;

	for (const auto& ch : It)
	{
		push_back(ch);
	}
}

赋值运算符重载 

对于赋值运算符重载函数,这里我们提供两种写法。

传统写法:如果不是自己给自己赋值,我们只需先清空原容器的数据,遍历所给容器,一个个尾插到原容器中即可。

//传统写法
list<T>& operator=(const list<T>& lt)
{
	if (*this != It)
	{
		//先清空原来容器里的数据
		clear();

		//遍历所给容器,将数据一个个尾插
		for (const auto& ch : It)
		{
			push_back(ch);
		}
	}

	return *this;//支持连续赋值
}

现代写法:直接使用swap函数进行交换。 

//现代写法
list<T>& operator=(const list<T>& lt)
{
	swap(It);

	return *this;//支持连续赋值
}

析构函数 

我们只需先将容器清空,释放头结点,并对其置空即可。

~list()
{
	//清空容器
	clear();
	//释放头结点
	delete _head;
	//置空
	_head = nullptr;
}

迭代器相关的函数 

begin和end

begin就是返回第一个有效节点,end直接返回头结点即可。

//迭代器相关函数
iterator begin()
{
	return iterator(_head->next);//返回第一个有效节点
}

iterator end()
{
	return iterator(_head);//返回头结点(哨兵位)
}

const_iterator begin() const
{
	return iterator(_head->next);//返回第一个有效节点
}

const_iterator end() const
{
	return iterator(_head);//返回头结点(哨兵位)
}

容量相关函数

size和empty

由此可见,我们在list类的成员变量中添加一个size,有很大的用处。

//容量相关函数
size_t size() const
{
	return size;
}
bool empty() const
{
	return size==0
}

容器访问相关函数

front和back

front就是返回第一个有效节点,back就是返回最后一个有效节点。

//访问容器相关函数
T& front()
{
	return *begin();//返回第一个有效节点
}
T& back()
{
	return *(--end());//返回最后一个有效节点
}
const T& front() const
{
	return *begin();//返回第一个有效节点
}
const T& back() const
{
	return *(--end());//返回最后一个有效节点
}

list的增删查改

insert

insert在所给迭代器之前插入一个新节点,我们在学习数据结构中的链表时,就是这套插入逻辑。记录好当前节点和前一个节点,用所给数据创建一个新节点,最后只需改变他们之间的指向关系即可。不要忘了给size++。

void insert(iterator pos, const T& x)
{
	assert(pos._node);//检查pos位置节点指针的合法性

	Node* pcur = pos._node;
	Node* prev = pcur->_prev;
	Node* newnode = new Node(x);
	//prev newnode pcur
	newnode->_next = pcur;
	pcur->_prev = newnode;

	newnode->_prev = prev;
	prev->_next = newnode;
	size++;
	
}

erase

erase删除所给迭代器位置的节点,我们在学习数据结构的链表时,删除节点也是这个逻辑。记录好当前节点的前一个节点和后一个节点,最后改变他们之间的指向关系即可。

void erase(iterator pos)
{
	assert(pos._node);//检查pos位置节点指针的合法性

	Node* pcur = pos._node;
	Node* prev = pcur->_prev;
	Node* next = pcur->_next;

	//prev  pcur next
	prev->_next = next;
	next->_prev = prev;
	size--;
}

push_back 和pop_back

push_back是尾插一个节点,pop_back是尾删一个节点。直接复用上面的函数即可。

void push_back(const T& x)
{
	insert(end(), x);
}

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

push_front 和 pop_front

push_front是头插一个节点,pop_front是头删一个节点。直接复用上面的函数即可。

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

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

clear

clear是清空容器,只需遍历删除即可。

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

swap

用库里的交换函数即可。

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

总结:

list的模拟实现与前面我们知道的string,vector的模拟实现有相似的地方,但也有所不同,我们用了三个类才成功模拟实现list。之后我们再来一起学习栈和队列的相关内容吧。

感谢各位大佬的观看,创作不易,还请各位大佬支持~ 

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

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

相关文章

helm3 部署项目应用示例

一、用到的插件 1、存储卷-日志外挂&#xff1a; 存储类自己提前建 2、env变量-存储nacos信息 二、新建项目 # helm create test-gateway 三、修改values.yaml ## 删除内容 # Additional volumes on the output Deployment definition. volumes: [] # - name: foo # se…

助力智能作物植株统计分析,基于YOLOv7全系列【tiny/l/x】参数模型开发构建田间作物场景下智能精准小麦麦穗检测识别计数系统

农业实验研究的一些场景下&#xff0c;尝尝有对指定视野区域内作物植株数量进行便捷化智能自动化统计计数的需求&#xff0c;诸如&#xff1a;棉花植株统计、小麦植株统计、水稻植株统计等等&#xff0c;这些农业实验场景下&#xff0c;单纯依靠人工数数的方式来进行植株计数是…

SpringBoot--yml配置文件的时间/大小的单位转换

原文网址&#xff1a;SpringBoot--yml配置文件的时间/大小的单位转换_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍SpringBoot的yml&#xff08;properties&#xff09;配置文件的时间/大小的单位转换。 概述 SpringBoot可以将yml中的配置绑定到一个Java类的字段&#x…

论文笔记——Graph Bottlenecked Social Recommendation

文章地址 代码地址 1.1简介 随着社交网络的出现&#xff0c;社交推荐已经成为个性化服务的重要技术。最近&#xff0c;基于图的社交推荐通过捕捉高阶社交影响显示出了有希望的结果。大多数基于图的社交推荐的经验研究直接将观察到的社交网络纳入公式&#xff0c;并基于社交同…

【注册/登录安全分析报告:孔夫子旧书网】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

香港科技大学数据建模MSc(DDM)硕士学位项目25/26招生宣讲会-西安专场

香港科技大学数据建模MSc(DDM)硕士学位项目25/26招生宣讲会-西安专场 &#x1f559;时间&#xff1a;2024 年10 月12日&#xff08;周六&#xff09; 16:00 &#x1f3e0;地点&#xff1a; 西安交大南洋大酒店(交通大学青龙寺店) 行政会议室 &#x1f9d1;‍&#x1f393;嘉宾…

0基础学习PyTorch——GPU上训练和推理

大纲 创建设备训练推理总结 在《Windows Subsystem for Linux——支持cuda能力》一文中&#xff0c;我们让开发环境支持cuda能力。现在我们要基于《0基础学习PyTorch——时尚分类&#xff08;Fashion MNIST&#xff09;训练和推理》&#xff0c;将代码修改成支持cuda的训练和推…

[sql-03] 求阅读至少两章的人数

准备数据 CREATE TABLE book_read (bookid varchar(150) NOT NULL COMMENT 书籍ID,username varchar(150) DEFAULT NULL COMMENT 用户名,seq varchar(150) comment 章节ID ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT 用户阅读表insert into book_read values(《太子日子》…

MindSearch 部署到Github Codespace 和 Hugging Face Space

和原有的CPU版本相比区别是把internstudio换成了github codespace。 教程是https://github.com/InternLM/Tutorial/blob/camp3/docs/L2/MindSearch/readme_github.md 复现步骤&#xff1a; 根据教材安装环境和创建硅基流动 API 然后启动前后端 然后按照教材部署到 Huggi…

安宝特案例 | 某知名日系汽车制造厂,借助AR实现智慧化转型

案例介绍 在全球制造业加速数字化的背景下&#xff0c;工厂的生产管理与设备维护效率愈发重要。 某知名日系汽车制造厂当前面临着设备的实时监控、故障维护&#xff0c;以及跨地域的管理协作等挑战&#xff0c;由于场地分散和突发状况的不可预知性&#xff0c;传统方式已无法…

计算机的错误计算(一百零六)

摘要 探讨含有变元负的整数次方的多项式的计算精度问题。 计算机的错误计算&#xff08;一百零五&#xff09;给出了一个传统多项式的错误计算案例&#xff1b;本节探讨含有变元负的整数次方的多项式的计算精度问题。 例1. 已知 计算 若在Python下计算&#xff0c;则有&…

猎板PCB大讲堂:PCB谐振效应及其对设计的影响

在PCB设计中&#xff0c;谐振效应是一个不可忽视的问题&#xff0c;它可能导致信号完整性问题、电源分配系统&#xff08;PDS&#xff09;工作异常&#xff0c;甚至成为EMI辐射源。以下是关于PCB谐振效应的一些详细信息&#xff1a; 1. 谐振产生的原因&#xff1a; - PCB中…

d2l | 目标检测数据集:RuntimeError: No such operator image::read_file

目录 1 存在的问题2 可能的解决方案3 最终的解决方案3.1 方案一&#xff08;我已弃用&#xff09;3.2 方案二&#xff08;基于方案一&#xff09;3.3 方案三&#xff08;基于方案一&#xff09; 1 存在的问题 李沐老师提供的读取香蕉数据集的函数如下&#xff1a; def…

Ubuntu系统设置bond双网卡

这里我的服务器是Ubuntu 22.04.3 LTS,是高阶版本,设置网卡需要通过netplan 根据你的Ubuntu版本(如使用Netplan或/etc/network/interfaces),选择相应的配置方法。 我这边以root用户登录进服务器,就不需要普通用户每次在命令前添加sudo 1.通常/etc/netplan下配置文件名形…

IDEA开发SpringBoot项目基础入门教程。包括Spring Boot简介、IDEA创建相关工程及工程结构介绍、书写配置文件、Bean对象管理等内容

文章目录 0. 关于本文1. 概述1.1 Spring简介1.2 Spring Boot简介1.3 传统的开发方式1.3.1 简述1.3.2 缺点 1.4 Spring Boot的优点 2. 创建一个简单的Spring Boot应用程序2.1 在IDEA创建项目2.2 pom配置文件内容2.3 启动类2.4 创建Controller 3. 从Maven工程创建Spring Boot工程…

数据结构~二叉搜索树

文章目录 一、二叉搜索树的概念二、二叉搜索树的结构二叉搜索树的性能分析二叉搜索树的插入二叉搜索树的查找二叉搜索树的删除 三、二叉搜索树key和key/value使用场景四、二叉搜索树的练习将二叉搜索树就地转化为已排序的双向循环链表从前序与中序遍历序列构造二叉树二叉树的前…

jmeter-请求参数加密-MD5加密

方法1 &#xff1a;使用jmeter自带的函数助手digest Tool(工具)---Function Helper Dialog(函数助手对话框) 第一个参数是要md5加密的值&#xff0c;第二个参数是保存加密后值的变量 &#xff08; 此处变量是从txt文件导入的&#xff0c;所以使用的是${wd} &#xff09; …

excel统计分析(1):列联表分析与卡方检验

列联表&#xff1a;用于展示两个或多个分类变量之间频数关系的表格。——常用于描述性分析卡方检验&#xff1a;通过实际频数和期望频数&#xff08;零假设为真情况下的频数&#xff09;&#xff0c;反映了观察频数与期望频数之间的差异程度&#xff0c;来评估两个变量是否独立…

Metasploit渗透测试之服务端漏洞利用

简介 在之前的文章中&#xff0c;我们学习了目标的IP地址&#xff0c;端口&#xff0c;服务&#xff0c;操作系统等信息的收集。信息收集过程中最大的收获是服务器或系统的操作系统信息。这些信息对后续的渗透目标机器非常有用&#xff0c;因为我们可以快速查找系统上运行的服…

System Timer (STM)

文章目录 1. 介绍2. 功能特性3. 应用场景4. 功能介绍4.1 TIME0 ~TIME6计数器精度与定时范围4.2 比较器工作原理4.3 中断处理 5. Ifx Demo5.1 STM_Interrupt_1_KIT_TC277_TFT5.2 STM_System_Time_1_KIT_TC275_LK5.3 SMU_Reset_Alarm_1_KIT_TC275_LK 1. 介绍 Ifx TC37x拥有3个自…