list结构刨析与模拟实现

news2025/2/24 5:46:15

目录

1.引言

2.C++模拟实现

2.1模拟实现结点

2.2模拟实现list前序

1)构造函数

2)push_back函数

2.3模拟实现迭代器

1)iterator

构造函数和析构函数:

*操作符重载函数:

前置/后置++/--:

==/!=操作符重载函数:

->操作符重载函数:

2)const_iterator

2.4模拟实现list后序

3)insert函数

4)erase函数

5)头插头删尾插尾删函数

6)size函数

7)begin/end函数

8)clear函数

9)析构函数

3.完整list


1.引言

先来说说为什么要模拟实现库里的list?

我认为,模拟实现list有以下几个意义和好处:

1. 学习:通过自己实现一个类似于STL中的list数据结构,可以加深对数据结构和算法的理解。通过亲自编码实现,能更好地理解list的内部工作原理,以及C++语言中与数据结构相关的特性和语法。

2. 练习:编写一个类似于STL中的list可以作为练习和提升编码能力的手段。这有助于加强对C++语言的掌握,并提高面向对象编程的技能。

3. 定制化需求:有时候,STL中提供的list功能不能完全满足特定需求,此时可以通过自己实现一个list类来扩展或定制list的功能,以更好地适应特定的应用场景。

2.C++模拟实现

      提前声明,由于list不同类型的函数重载太多太杂,本篇仅仅模拟实现简单的构造,析构,操作符重载,深浅拷贝,增删查改等部分函数的介绍,感谢读者的支持!

      建议先创建一个list.hpp头文件,单独创建一个命名空间,防止已经展开了std的命名空间,实现的list与库中list发生冲突。

      我们就定义命名空间为drw,接下来完成三个部分的编写:

2.1模拟实现结点

思路:

  1. 我们先来定义一个结构体__list_node来代表list中存放数据的结点,方便list中函数操作等等访问要求
  2. 设置一个模板class T来接受各种类型的显式实例化
  3. 这里不能将__list_node直接命名为Node是为了防止与其他同名结构体冲突
  4. 设置三个成员变量,分别为:prev    next    val,用来储存前后结点以及此处结点的数值
  5. 使用初始化列表完成构造和析构函数

实现:

template<class T>
struct __list_node
{
	__list_node<T>* prev;
	__list_node<T>* next;
	T val;

	__list_node(const T& t=T())
		:prev(nullptr)
		,next(nullptr)
		,val(t)
	{}

	~__list_node()
	{
		prev = nullptr;
		next = nullptr;
		val = 0;
	}
};

2.2模拟实现list前序

      由于list同样需要模板class T来显式实例化,那么我们先设置一个class T模板参数,为什么是先呢?是因为后面还会有补充,请耐心看完~

      因为list不希望结点被外界访问,将结点进行了封装,所以可以先将__list_node重命名为Node来方便表示,并且只能在public之外重命名,list类中只有一个私有成员变量,就是Node* _head,这代表头结点。接下来完成成员函数:

1)构造函数

思路:

  1. 这里我们实现两个构造函数,分别是直接构造和拷贝构造
  2. 因为这两种构造都要先初始化一个头结点,我们不妨设置一个Emptyinit函数来做这个事情,方便复用
  3. 在Emptyinit函数中,先new一个Node,对val不做处理,让prev指向自己,next也指向自己
  4. 直接构造:就是Emptyinit函数的过程,直接复用
  5. 拷贝构造:参数是const list<T>& l,除了调用Emptyinit之外还要调用push_back对新的list进行尾插拷贝,这里的push_back后续会讲解

实现:

void Emptyinit()
{
	Node* guard = new Node;
	guard->prev = guard;
	guard->next = guard;
	_head = guard;
}

list()
{
	Emptyinit();
}

list(const list<T>& l)
{
	Emptyinit();
	for (auto& e : l)//加上&防止自定义类型深拷贝
	{
		push_back(e);
	}
}

2)push_back函数

思路:

  1. 先new一个新结点node,将t传进去初始化node
  2. 再将新结点的prev设置为_head的prev,next为_head
  3. 更新_head的prev以及原先_head之前结点的next

实现:

void push_back(const T& t)
{
	Node* newnode = new Node(t);
	newnode->prev = _head->prev;
	_head->prev->next = newnode;
	newnode->next = _head;
	_head->prev = newnode;//双向带头循环链表,需要复习!
}
void push_back(const T& t)
{
	insert(_head, t);
}

这里还可以直接调用insert函数,后面介绍!由于后续函数需要迭代器,这里穿插介绍模拟实现迭代器:

2.3模拟实现迭代器

      在使用list的时候,我们知道迭代器可以++/--,但是不能+/-,因为list迭代器属于双向迭代器但不属于随机迭代器,但每个结点存储位置是分散开的啊,这怎么实现++/--呢,于是可以定义一个迭代器结构体,将其封装成类,就可以进行这一操作了!

      设置模板template<class T>,定义__list_iterator,为了方便表示__list_node,这里也提前重命名一下为Node,设置成员变量为node

1)iterator

构造函数和析构函数:

思路:直接使用初始化列表进行赋值nullptr即可,同样赋值nullptr进行析构,因为node已经有默认构造和析构,就不需要更多的处理

实现:

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

~__list_iterator()
{
	node = nullptr;
}
*操作符重载函数:

思路:直接返回node代表的val即可

实现:

T& operator*()
{
	return node->val;
}
前置/后置++/--:

思路:++:前置就让node指向next即可,后置就拷贝tmp,让node指向next返回tmp

           --:前置node指向prev,后置拷贝tmp,node指向prev返回tmp

实现:

__list_iterator<T>& operator++()//前置
{
	node = node->next;
	return *this;
}

__list_iterator<T>& operator++(int)//后置
{
	__list_iterator<T> tmp(*this);
	node = node->prev;
	return tmp;
}

__list_iterator<T>& operator--()//前置
{
	node = node->next;
	return *this;
}

__list_iterator<T>& operator--(int)//后置
{
	__list_iterator<T> tmp(*this);
	node = node->prev;
	return tmp;
}
==/!=操作符重载函数:

思路:判断一下是否相等即可

实现:

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

bool operator==(const __list_iterator<T>& it)
     	{
			return node == it.node;
		}
->操作符重载函数:

思路:为什么要有这个函数?是因为如果list存储的是含有多个成员变量的结构体,那么想要访问成员变量不应该仅仅有*.还应该提供->

      这里直接返回T*类型的&(node->val)即可就不进行实现展示了。

2)const_iterator

思考:const迭代器与iterator相差在哪里呢?无非就是*操作符重载函数多了一些const,其他大致相同,所以我们就不必再去大费周章重新写,这里增加一个模板参数Ref,在显式实例化的时候传T&或者const T&就可以解决这个问题:

       那么这仅仅解决了*函数的重载问题,->函数呢?这当然又需要一个模板参数Ptr,传参方法是一样的。为了简化类型,将 __list_iterator<T, Ref,Ptr> 重命名为self

演示整个迭代器:

template<class T,class Ref,class Ptr>//为什么要传Ref是因为两个结构体太累赘,这样可以简化,要传Ptr是为了给->函数的返回类型也进行模板化
struct __list_iterator
{
	typedef __list_node<T> Node;
	typedef __list_iterator<T, Ref,Ptr> self;//这里再次重定义一下方便
	Node* node;

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

	Ref operator*()
	{
		return node->val;
	}

	Ptr operator->()//为什么要重载访问成员操作符呢?是因为显式实例化传参也就是vector里面可能保存的是自定义类型而不是内置类型
	{
		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->prev;
		return *this;
	}

	self& operator--(int)//后置
	{
		self tmp(*this);
		node = node->prev;
		return tmp;
	}

	bool operator!=(const self& it)
	{
		return node != it.node;
	}

	bool operator==(const self& it)
	{
		return node == it.node;
	}

	~__list_iterator()
	{
		node = nullptr;
	}
};

2.4模拟实现list后序

3)insert函数

思路:

  1. insert函数就是在迭代器位置为pos的地方插入数据
  2. 开辟一个新结点newnode,将数据传入初始化,记录一下pos的前一个结点地址prev
  3. 让prev的next指向newnode,newnode的prev指向prev,newnode的next指向pos,pos的prev指向newnode,返回newnode

实现:

iterator insert(iterator pos, const T& t)
{
	//无需断言
	Node* prev = pos.node->prev;
	Node* newnode = new Node(t);
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos.node;
	pos.node->prev = newnode;
	return newnode;
}

4)erase函数

思路:

  1. 判断一下pos是否合法,不能删除头结点
  2. 记录一下前一个和后一个结点地址,分别为prev和next
  3. 让prev的next指向next,next的prev指向prev
  4. 释放掉pos结点,返回next的地址,这是防止迭代器失效的措施

实现:

iterator erase(iterator pos)
{
	assert(pos != end());//不能删除头结点
	Node* prev = pos.node->prev;
	Node* next = pos.node->next;
	prev->next = next;
	next->prev = prev;
	delete pos.node;
	return next;
}

5)头插头删尾插尾删函数

思路:

  1. 由于实现了insert和erase,这里直接复用就方便多了

实现:

void push_back(const T& t)
{
	insert(_head, t);
}

void pop_back()
{
	erase(_head->prev);
}

void push_front(const T& t)
{
	insert(_head->next, t);
}

void pop_front()
{
	erase(_head->next);
}

6)size函数

思路:

  1. 要计算链表中的结点个数,但不能算入头结点
  2. 定义size_t sz,从头结点的下一个开始遍历,到指针指向_head结束

实现:

size_t size()
{
	size_t sz = 0;
	iterator it = begin();
	while (it != end())
	{
		sz++;
		it++;
	}
	return sz;
}

7)begin/end函数

思路:

  1. 直接将_head的下一个结点或者_head返回,因为end代表最后一个元素的下一个位置

实现:

iterator begin()
{
	return _head->next;
}

iterator end()
{
	return _head;
}

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

const_iterator end()const
{
	return _head;
}

8)clear函数

思路:

  1. clear函数是将list中的每一个结点进行释放删除,相当于清空链表
  2. 用循环实现即可,但是要注意迭代器的失效问题!erase之后的迭代器要及时更新

实现:

void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it=erase(it);//迭代器失效了!!!要注意!!!
	}
}

9)析构函数

思路:

  1. 调用clear函数之后释放掉头结点就完成了析构

实现:

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

3.完整list

这里给出完整的实现代码:

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace drw
{
	template<class T>
	struct __list_node
	{
		__list_node<T>* prev;
		__list_node<T>* next;
		T val;

		__list_node(const T& t=T())
			:prev(nullptr)
			,next(nullptr)
			,val(t)
		{}

		~__list_node()
		{
			prev = nullptr;
			next = nullptr;
			val = 0;
		}
	};

	template<class T,class Ref,class Ptr>//为什么要传Ref是因为两个结构体太累赘,这样可以简化,要传Ptr是为了给->函数的返回类型也进行模板化
	struct __list_iterator
	{
		typedef __list_node<T> Node;
		typedef __list_iterator<T, Ref,Ptr> self;//这里再次重定义一下方便
		Node* node;

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

		Ref operator*()
		{
			return node->val;
		}

		Ptr operator->()//为什么要重载访问成员操作符呢?是因为显式实例化传参也就是vector里面可能保存的是自定义类型而不是内置类型
		{
			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->prev;
			return *this;
		}

		self& operator--(int)//后置
		{
			self tmp(*this);
			node = node->prev;
			return tmp;
		}

		bool operator!=(const self& it)
		{
			return node != it.node;
		}

		bool operator==(const self& it)
		{
			return node == it.node;
		}

		~__list_iterator()
		{
			node = nullptr;
		}
	};
	//这样实现太过于累赘,最好再添加一个模版参数来实现
	//template<class T>
	//struct __const_list_iterator
	//{
	//	typedef __list_node<T> Node;
	//	Node* node;

	//	__const_list_iterator(Node* _node)
	//		:node(_node)
	//	{
	//	}

	//	const T& operator*()const
	//	{
	//		return node->val;
	//	}

		//__const_list_iterator<T>& operator++()//前置
		//{
		//	node = node->next;
		//	return *this;
		//}

		//__const_list_iterator<T>& operator++(int)//后置
		//{
		//	__const_list_iterator<T> tmp(*this);
		//	node = node->next;
		//	return tmp;
		//}

	//	bool operator!=(const __const_list_iterator<T>& it)
	//	{
	//		return node != it.node;
	//	}

	//	bool operator==(const __const_list_iterator<T>& it)
	//	{
	//		return node == it.node;
	//	}

	//	~__const_list_iterator()
	//	{
	//		node = nullptr;
	//	}
	//};

	template<class T>
	class list
	{
		typedef __list_node<T> Node;
	public:
		/*typedef __list_iterator<T> iterator;
		typedef __const_list_iterator<T> const_iterator;*/
		//typedef const __list_iterator<T> const_iterator;//不能这样使用 const迭代器那么迭代器就不能改变了
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

		void Emptyinit()
		{
			Node* guard = new Node;
			guard->prev = guard;
			guard->next = guard;
			_head = guard;
		}

		list()
		{
			Emptyinit();
		}

		list(const list<T>& l)
		{
			Emptyinit();
			for (auto& e : l)//加上&防止自定义类型深拷贝
			{
				push_back(e);
			}
		}

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

		//void push_back(const T& t)
		//{
		//	Node* newnode = new Node(t);
		//	newnode->prev = _head->prev;
		//	_head->prev->next = newnode;
		//	newnode->next = _head;
		//	_head->prev = newnode;//双向带头循环链表,需要复习!
		//}
		void push_back(const T& t)
		{
			insert(_head, t);
		}

		void pop_back()
		{
			erase(_head->prev);
		}

		void push_front(const T& t)
		{
			insert(_head->next, t);
		}

		void pop_front()
		{
			erase(_head->next);
		}

		iterator begin()
		{
			return _head->next;
		}

		iterator end()
		{
			return _head;
		}

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

		const_iterator end()const
		{
			return _head;
		}

		iterator insert(iterator pos, const T& t)
		{
			//无需断言
			Node* prev = pos.node->prev;
			Node* newnode = new Node(t);
			prev->next = newnode;
			newnode->prev = prev;
			newnode->next = pos.node;
			pos.node->prev = newnode;
			return newnode;
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());//不能删除头结点
			Node* prev = pos.node->prev;
			Node* next = pos.node->next;
			prev->next = next;
			next->prev = prev;
			delete pos.node;
			return next;
		}

		size_t size()
		{
			size_t sz = 0;
			iterator it = begin();
			while (it != end())
			{
				sz++;
				it++;
			}
			return sz;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it=erase(it);//迭代器失效了!!!要注意!!!
			}
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
	private:
		Node* _head;
	};
}

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

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

相关文章

react 踩坑记 too many re-renders.

报错信息&#xff1a; too many re-renders. React limits the number of randers to prevent an infinite loop. 需求 tabs只有特定标签页才展示某些按钮 button要用 传递函数引用方式 ()>{} *还有要注意子组件内loading触发 导致的重复渲染

BGP分解实验·19——BGP选路原则之起源

当用不同的方式为BGP注入路由时&#xff0c;起源代码将标识路由的来源。 &#xff08;在BGP表中&#xff0c;Network为“i”&#xff0c;重分布是“&#xff1f;”&#xff09; 实验拓扑如下&#xff1a; R2上将来自IGP的路由10.3.3.3/32用network指令注入BGP;在R4上将来自I…

单机上使用docker搭建minio集群

单机上使用docker搭建minio集群 1.集群安装1.1前提条件1.2步骤指南1.2.1安装 Docker 和 Docker Compose&#xff08;如果尚未安装&#xff09;1.2.2编写docker-compose文件1.2.3启动1.2.4访问 2.使用2.1 mc客户端安装2.2创建一个连接2.3简单使用下 这里在ubuntu上单机安装一个m…

家用路由器的WAN口和LAN口有什么区别

今时今日&#xff0c;移动终端盛行的时代&#xff0c;WIFI可以说是家家户户都有使用到的网络接入方式。那么路由器当然也就是家家户户都不可或缺的设备了。而路由器上的两个实现网络连接的基础接口 ——WAN 口和 LAN 口&#xff0c;到底有什么区别&#xff1f;它们的功能和作用…

实操解决Navicat连接postgresql时出现‘datlastsysoid does not exist‘报错的问题

1 column “datlastsysoid“ does not exist2 Line1:SELECT DISTINCT datalastsysoid FROM pg_database问题分析 Postgres 15 从pg_database表中删除了 datlastsysoid 字段引发此错误。 决绝方案 解决方法1&#xff1a;升级navicat 解决方法2&#xff1a;降级pgsql 解决方…

3分钟idea接入deepseek

DeepSeek简介 DeepSeek 是杭州深度求索人工智能基础技术研究有限公司开发的一系列大语言模型&#xff0c;背后是知名量化资管巨头幻方量化3。它专注于开发先进的大语言模型和相关技术&#xff0c;拥有多个版本的模型&#xff0c;如 DeepSeek-LLM、DeepSeek-V2、DeepSeek-V3 等&…

树莓派理想二极管电路分析

如果 Vin Vout&#xff0c;比如说 5.0V&#xff0c;PNP 晶体管以当前的镜像配置偏置。晶体管 U14 的 Vb 将为 5-0.6 4.4V&#xff0c;镜像配置意味着 Vg 也将为 4.4V. Vgs 为4.4-5.0 -0.6V。mosfet 将处于关闭状态&#xff08;几乎打开&#xff09;。如果 Vout 略低于 Vin&a…

Unity贴图与模型相关知识

一、贴图 1.贴图的类型与形状 贴图类型 贴图形状 2.在Unity中可使用一张普通贴图来生成对应的法线贴图&#xff08;但并不规范&#xff09; 复制一张该贴图将复制后的贴图类型改为Normal Map 3.贴图的sRGB与Alpha sRGB&#xff1a;勾选此选项代表此贴图存储于Gamma空间中…

Linux--进程(进程虚拟地址空间、页表、进程控制、实现简易shell)

一、进程虚拟地址空间 这里以kernel 2.6.32&#xff0c;32位平台为例。 1.空间布局 在 32 位系统中&#xff0c;虚拟地址空间大小为 4GB。其中&#xff1a; 内核空间&#xff1a;占据高地址的 1GB &#xff0c;用于操作系统内核运行&#xff0c;包含内核代码、内核数据等&am…

中间件专栏之redis篇——redis基本原理、概念及其相关命令介绍

一、redis是什么 redis是remote dictionary service的简称&#xff0c;中文翻译为远程字典服务&#xff1b; redis是一种数据库&#xff0c;若按照类型来归类&#xff0c;则其可以被归入三个类型数据库&#xff0c;分别为&#xff1a;内存数据库、KV数据库、数据结构数据库&a…

在列线图上标记做为线性模型的局部解释

改造列线图做为线性模型的解释 除了使用列线图算法产生的meta数据和score数据进行线性模型的解释&#xff08;全局性解释和局部性解释&#xff09;&#xff0c;另外一种做法是改造列线图来作为线性模型的解释。这里尝试改造列线图来对线性模型进行全局性和局部性解释。 全局…

KubeKey一键安装部署k8s集群和KubeSphere详细教程

目录 一、KubeKey简介 二、k8s集群KubeSphere安装 集群规划 硬件要求 Kubernetes支持版本 操作系统要求 SSH免密登录 配置集群时钟 所有节点安装依赖 安装docker DNS要求 存储要求 下载 KubeKey 验证KubeKey 配置集群文件 安装集群 验证命令 登录页面 一、Ku…

车载诊断数据库 --- AUTOSAR诊断文件DEXT简介

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活,除了生存温饱问题之外,没有什么过多的欲望,表面看起来很高冷,内心热情,如果你身…

庙算兵棋推演AI开发初探(5-数据处理)

碎碎念&#xff1a;这最近几个月过得那叫一个难受&#xff0c;研究生开题没过、需求评审会在4月和6月开了2次、7月紧接着软件设计评审会&#xff0c;加班干得都是文档的事情&#xff0c;还有开会前的会务和乱七八糟的琐事&#xff0c;我们干的还被规定弄的束手束脚&#xff0c;…

【MyBatis】#{} 与 ${} 的区别(常见面试题)

目录 前言 预编译SQL和即时SQL 什么是预编译SQL&#xff1f; 什么是即时SQL&#xff1f; 区别 #{} 与 ${}的使用 防止SQL注入 什么是SQL注入&#xff1f; 原理 排序功能 模糊查询 总结#{}和${}的区别 前言 在前面的学习中&#xff0c;我们已经知道了如果SQL语句想…

鸿蒙开发环境搭建-入门篇

本文章讲述如何搭建鸿蒙应用开发环境&#xff1a;新建工程、虚拟机运行、真机调试等。 开发工具: DevEco Studio 5.0.3.906 os系统: mac 参考文档&#xff1a;https://juejin.cn/post/7356143704699699227 官网鸿蒙应用开发学习文档&#xff1a;https://developer.huawei.com/c…

iOS开发 网络安全

iOS开发中的网络安全 在当前的数字化时代&#xff0c;任何应用程序都需要重视网络安全。尤其是对于iOS应用开发者而言&#xff0c;确保应用与服务器之间的数据传输安全是至关重要的。接下来&#xff0c;我们将学习“iOS开发 网络安全”的实现过程。 流程步骤 以下是实现iOS网…

MATLAB在投资组合优化中的应用:从基础理论到实践

引言 投资组合优化是现代金融理论中的核心问题之一&#xff0c;旨在通过合理配置资产&#xff0c;实现风险与收益的最佳平衡。MATLAB凭借其强大的数学计算能力和丰富的金融工具箱&#xff0c;成为投资组合优化的理想工具。本文将详细介绍如何使用MATLAB进行投资组合优化&#…

银河麒麟系统安装mysql5.7【亲测可行】

一、安装环境 cpu&#xff1a;I5-10代&#xff1b; 主板&#xff1a;华硕&#xff1b; OS&#xff1a;银河麒麟V10&#xff08;SP1&#xff09;未激活 架构&#xff1a;Linux 5.10.0-9-generic x86_64 GNU/Linux mysql版本&#xff1a;mysql-5.7.34-linux-glibc2.12-x86_64.ta…

自动创建spring boot应用(eclipse版本)

使用spring starter project创建项目 设置Service URL 把Service URL设置为 https://start.aliyun.com/ 如下图&#xff1a; 使用这个网址&#xff0c;创建项目更快。 选择Spring Web依赖 项目结构 mvnw和mvnw.cmd:这是maven包装器&#xff08;wrapper&#xff09;脚本&…