【STL】list的模拟实现

news2024/11/26 4:43:51

目录

前言

结构解析

默认成员函数

构造函数

拷贝构造

赋值重载

析构函数

迭代器

const迭代器

数据修改

insert

erase

尾插尾删头插头删

容量查询

源码 


前言

🍉list之所以摆脱了单链表尾插麻烦,只能单向访问等缺点,正是因为其在结构上升级成了带头双向循环链表。不仅如此,list中迭代器的实现更是能拓宽我们对迭代器的认识,话不多说,马上开始今天的内容。

结构解析

🍉以前我们实现单链表的时候就只定义了节点的结构体,之后传回第一个节点就作为首个节点直接开始使用。而今天我们要封装的是一个 list,因此需要用一个类代表整个 list,之后还需要再定义一个类来表示节点。看下下图,可以了解得比较直观一些。

🍉之后我们将二者放进命名空间中,避免与库中的 list 重复。在这之中,由于结构是带头双向循环链表,所以一个节点中需要存放前后节点的指针当前节点的数据,而头结点作为链表的首位,因此 list 中只要存头结点的指针即可

namespace Alpaca
{
	template <class T>
	struct list_node         //节点的类
	{
		list_node* _next;
		list_node* _prev;
		T _data;
	};

    template <class T>
	class list           //list的类
	{
	public:
		typedef list_node<T> node; 
    	private:
		node* _head;
	};
}

🍉提到一个容器,除了它的数据结构之外,迭代器的组成也是十分重要,之前模拟实现 string 和 vector 的时候,我们都是使用原生指针作为迭代器进行使用的,这是因为二者都是在连续的空间存储的直接 ++ 指针便可以直接访问到下一个数据。但链表的各个节点并不是开辟在相邻的空间之中,而是使用节点中的指针访问下一节点的,因此继续用原生指针 ++ 并不能达到访问的目的。所以我们需要手动封装一个迭代器来满足访问数据的需求。

template <class T, class Ref, class Ptr>
struct list_iterator
{
	typedef list_node<T> node;
	typedef list_iterator<T, Ref, Ptr> self; 

    node* _node;
};

🍉至于为什么模板参数那么多,在下面迭代器部分会详细讲解。

默认成员函数

🍉即便 list 中没有其余数据,但可以确定其中至少有一个头结点,因此不论是什么类型的构造函数,都要对头结点进行初始化。为了简化代码,不妨将这个操作单独写成一个函数

void empty_init()
{
	_head = new node;        //开辟节点
	_head->_next = _head;    //维持循环关系
	_head->_prev = _head;
}

构造函数

🍉与库中的构造函数靠齐,这里我们还是实现三种构造函数。

  • 无参数构造
  • n个值构造
  • 迭代器区间构造

🍉第一个无参数构造实际上只需要初始化头结点即可,因此直接调用上面实现的函数即可。

list()
{
	empty_init();
}

🍉第二个构造则是以 n 个值来构造,因此只需要限定次数后依次插入相同数据即可。

🍉值得注意的一点是,传入的数据类型可能是自定义类型,因此不能使用浅拷贝,而必须使用深拷贝(即调用类型本身的构造函数)。

🍉因此这里使用了 push_back,具体实现在数据修改部分讲解,本质就是调用原数据类型的拷贝构造生成一个新的数据

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

🍉使用迭代器区间进行拷贝,但是这个迭代器并不一定是 list 的迭代器,可能是 vector 的也可能是 string 的。所以要再使用一个模板来表示迭代器的类型。因此这里同样要使用深拷贝才能确保不出错。

template<class InputIterator>
list(InputIterator first, InputIterator last)
{
	empty_init();
	while (first != last)    //遍历迭代器区间
	{
		push_back(*first);
		first++;
	}
}

拷贝构造

🍉传入一个 list,需要复制一个值一样的 list 出来,但转念一想不就跟上面使用迭代器区间构造一样吗?

list(const list<T>& l)
{
	empty_init();
	const_iterator cur = l.begin();
	while (cur != l.end())
	{
		push_back(*cur);
		cur++;
	}
}

🍉咻一下,很快啊代码就写好了。 这时候我们不禁想,既然代码重合度那么高,我们能否能够复用迭代器区间构造来实现拷贝构造呢?

🍉在这之前,有一个重要的拼图我们还没有实现,那就是交换函数,实际上我们通过复用库中的交换函数交换头结点即可。

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

🍉下面就是见证奇迹的时刻,我们通过迭代器区间构造出一个临时的 list,之后将临时的 list 和当前 list 交换,即完成拷贝构造。

list(const list<T>& l)
{
    empty_init();
	list<T> tmp(l.begin(), l.end());
	swap(tmp);
}

🍉函数结束后,tmp 就会调用析构,释放原来 list 中的节点。

赋值重载

🍉之前 vector 和 string 实现之中,我们也是直接使用了这个方法,直接传值调用赋值重载,之后交换两个 list 即可。

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

析构函数

🍉list 与 vector 不一样,析构 list 时需要释放链表中的所有节点,之后再销毁头结点,因此可以单独写一个 clear 函数释放节点。

🍉从头开始删除节点即可,为了避免迭代器失效,因此要接收 erase 的返回值

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

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

迭代器

🍉在上面,我们讲过我们要对迭代器进行封装来满足实际访问时的需求,迭代器内部实际上存着一个节点的指针

🍉一般都是以节点地址作为参数来构建迭代器,而当拷贝构造时,直接赋值即可。

🍉这是因为迭代器本身起一个访问的效果,与两个指向同一地址的指针的地址并不相同是同一个道理。

list_iterator(node* n = nullptr)   //默认构造
	:_node(n)
{}

list_iterator(const self& s)       //拷贝构造
	:_node(s._node)
{}

🍉同时这个迭代器的类并不需要写析构函数,因为在这个类中我们也并没有手动申请空间需要手动释放等情况,使用默认的析构即可

🍉作为一个迭代器,我们自然要放眼于其的迭代区间,begin 和 end 的范围该如何判断

🍉再看了一眼结构图,其中头结点自然是不能访问的,头结点的下一个节点才是有效节点,那什么时候才能算访问完整个 list 呢?

🍉由于带头双向循环链表中是不会出现 nullptr 的,因此在访问完所有数据后就会回到头结点

🍉因此 begin 就是头节点的下一位,而 end 就是头节点,同时这里使用匿名对象是为了贴合编译器的优化方式

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

iterator end()
{
	return iterator(_head);
}

🍉之后就是对运算符进行重载了,结合实际的使用,++ 就是转换成下一个节点,-- 就是转换成上一个节点。

🍉通过对节点地址的比较便可实现两个迭代器的比较

🍉其中 self 类型就是 typedef 之后的当前迭代器类型。

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

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

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

self& operator --()
{
	_node = _node->_prev;
	return *this;
}
		
bool operator != (const self& s) const
{
	return _node != s._node;
}

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

const迭代器

🍉这时候,有人看了一眼迭代器的模板参数,问说为什么这个迭代器需要三个模板参数呢?

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

🍉若我们直接像上面进行重载,在我们使用 const 迭代器时就会出现权限放大的情况。

🍉那在 iterator 前再加上 const 呢?虽然不会报错,但是实际使用起来,并不能保护我们的数据不被修改

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

🍉这是由于在只有一个模板参数的情况下, iterator 的原型就是 list_iterator<T> 而加了一个 const 就代表保护类中的指针不被修改,但实际上我们要保护不被修改的对象是类中指针指向的那个数据,因此直接在 iterator 前加 const 是没有效果的。这个时候摆在我们面前就只有两个选择了,一是专门写一份const 迭代器,二则是增加一个迭代器模板参数在使用时传入一个 const T 供我们使用。

[注意]: 直接在迭代器代码中设置返回 const 那也是不可以的,因为不是所有的链表都被限定不再修改。

🍉同样的道理不仅在 & 中并且在 * 类型中的数据也要确保能被 const 保护起来,因此,在list中便可以这样定义。

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

🍉迭代器的模板为 

template <class T, class Ref, class Ptr>

🍉如此,我们便可以将上面漏掉的 * 和 -> 重载给补上了。

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

Ptr operator ->()
{
	return &(operator*());
}

🍉值得注意的是,这里对 -> 的解引用其实是经过编译器优化的,实际上应该是需要使用两个 -> 一个用于调用重载一个用于访问类中的成员。之所以进行优化就是为了贴合使用增加可读性

假设代码这样使用    l->val
转换成            &(_node->_data)->val
实际上            l->->val    

 🍉之后我们还要在 list 中加上获取 const 类型迭代器的版本。

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

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

数据修改

insert

🍉用指定一个位置,在这个位置插入一个节点,这里我们就不需要向 vector 那样挪动数据了,只需要更改节点中的指针即可。

void insert(iterator pos, const T& x = T())
{
	node* cur = pos._node;          //当前位置节点
	node* prev = cur->_prev;        //上一个节点
	node* new_node = new node(x);   //新的节点
	prev->_next = new_node;         //更改指向
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

🍉下图中,黑色代表原来指针,红色代表之后各个节点的指向。 由于 list 中节点各自不互相影响,因此不会出现扩容的情况,即 insert 后迭代器并不会失效。

erase

🍉erase 用于删除指定位置的节点,我们通过获取当前节点前后节点,更改二者指向后再删除当前节点。

🍉由于 erase 面临迭代器失效的问题,因此在最后我们需要返回指向下一位的迭代器用于更新。

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 iterator(next);      //返回下一个节点
}

尾插尾删头插头删

🍉通过对前面 insert 和 erase 的复用,我们便可以轻松地完成 尾插尾删 和 头插头删 的实现。

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

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

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

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

容量查询

🍉查询 list 的容量只需要数一数节点的个数,我们遍历一遍 list 就能够得到结果。

size_t size() const
{
	size_t count = 0;
	for (auto it = begin(); it != end(); it++)
	{
		count++;
	}
	return count;
}

🍉而判空就更加简单了,只要判断头结点的下一位是否还是头结点即可。

bool empty() const
{
	return _head->_next == _head;
}

源码 

🍉感兴趣的可以来这里看看源码。

代码仓库


🍉好了,今天 list模拟实现 的相关内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。

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

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

相关文章

日常 - HttpURLConnection 网络请求 TLS 1.2

文章目录 环境前言HTTPS 请求流程服务端支持JDK 验证资源 环境 JDK 8 Hutool 4.5.1 前言 应供应商 DD 的 TLS 版本升级通知&#xff0c;企业版接口升级后 TLS 1.0 及 1.1 版本请求将无法连接&#xff0c;仅支持 TLS 1.2 及以上版本的客户端发起请求。 当前项目使用 Hutool …

有序表2:跳表

跳表是一个随机化的数据结构&#xff0c;可以被看做二叉树的一个变种&#xff0c;它在性能上和红黑树&#xff0c;AVL树不相上下&#xff0c;但是跳表的原理非常简单&#xff0c;目前在Redis和LeveIDB中都有用到。 它采用随机技术决定链表中哪些节点应增加向前指针以及在该节点…

找不到“SqlServer”模块-- 在此计算机上找不到任何 SQL Server cmdlet。

https://github.com/PowerShell/PowerShell/releases/tag/v7.2.2SQL Server Management Studio 18 启动触发器报错 标题: 找不到“SqlServer”模块 --------------- 在此计算机上找不到任何 SQL Server cmdlet。 在 https://powershellgallery.com/packages/SqlServer 上获取“…

PyTorch深度学习实战(1)——神经网络与模型训练过程详解

PyTorch深度学习实战&#xff08;1&#xff09;——神经网络与模型训练过程详解 0. 前言1. 传统机器学习与人工智能2. 人工神经网络基础2.1 人工神经网络组成2.2 神经网络的训练 3. 前向传播3.1 计算隐藏层值3.2 执行非线性激活3.3 计算输出层值3.4 计算损失值3.5 实现前向传播…

Linux——应用层之序列号与反序列化

TCP协议通讯流程 tcp是面向连接的通信协议,在通信之前,需要进行3次握手,来进行连接的建立。 当tcp在断开连接的时候,需要释放连接,4次挥手 服务器初始化: 调用socket, 创建文件描述符; 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了…

【机器学习】9种回归算法及实例总结,建议学习收藏

我相信很多人跟我一样&#xff0c;学习机器学习和数据科学的第一个算法是线性回归&#xff0c;它简单易懂。由于其功能有限&#xff0c;它不太可能成为工作中的最佳选择。大多数情况下&#xff0c;线性回归被用作基线模型来评估和比较研究中的新方法。 在处理实际问题时&#…

VirtualBox安装增强功能

在刚安装完的VisualBox中&#xff0c;默认屏幕是固定设置的&#xff0c;不会根据实际的窗口大小做自适应&#xff0c;这时候我们需要【安装增强功能】&#xff0c;然后打开【自动调整显示大小】&#xff0c;就可以实现虚拟机中屏幕自适应。 本教程的软件环境如下&#xff1a; 宿…

数据结构: 第四章 串

文章目录 一、串的定义和实现1.1串的定义和基本操作1.1.1串的定义1.1.2串的基本操作1.1.3小结 1.2串的存储结构1.2.1顺序存储1.2.2链式存储1.2.3基于顺序存储实现基本操作1.2.4小结 二、串的模式匹配2.1什么是字符串的模式匹配2.2朴素模式匹配算法2.3KMP算法2.4求next数组2.5KM…

python+django协同过滤算法的美食O2O外卖点餐系统vue

当然使用的数据库是mysql。尽管没有面向对象的数据库的作用强大&#xff0c;但是在Python开发上还是比较的灵活和方便的。系统功能主要介绍以下几点&#xff1a; 本外卖点餐系统主要包括二大功能模块&#xff0c;即用户功能模块和管理员功能模块。 &#xff08;1&#xff09;管…

Linux上安装jdk Tomcat mysql redis

1.安装JDk 1.1这里使用xshell中xfxp进行文件的上传&#xff0c;将jdk二进制包上传到Linux服务器上 下载地址&#xff1a;Java Downloads | Oracle 或者这里有下载好的安装包&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1ZSJxBDzDaTwCH2IG-d2Gig 提取码&#xff1a;…

dubbo 3.2.0 consumer bean初始化及服务发现简记

consumer bean初始化 以spring 如下配置<dubbo:reference id"versionConsumerBean" interface"org.apache.dubbo.samples.version.api.VersionService" version"*"/>为例&#xff0c;先使用spring 的初始化&#xff0c;核心代码 try {fin…

EDR(端点、端点检测与响应中心、可视化展现)

EDR基本原理与框架 EDR定义 端点检测和响应是一种主动式端点安全解决方案&#xff0c;通过记录终端与网络事件&#xff08;例如用户&#xff0c;文件&#xff0c;进程&#xff0c;注册表&#xff0c;内存和网络事件&#xff09;&#xff0c;并将这些信息本地存储在端点或集中数…

C#,码海拾贝(26)——求解“一般带状线性方程组banded linear equations”之C#源代码,《C#数值计算算法编程》源代码升级改进版

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 求解线性方程组的类 LEquations /// 原作 周长发 /// 改编 深度混淆 /// </summary> public static partial class LEquations { /// <summary> /…

Redis五大基本数据结构(原理)

一、 Redis数据结构-String String是Redis中最常见的数据存储类型&#xff1a; 其基本编码方式是RAW&#xff0c;基于简单动态字符串&#xff08;SDS&#xff09;实现&#xff0c;存储上限为512mb。 如果存储的SDS长度小于44字节&#xff0c;则会采用EMBSTR编码&#xff0c;…

c++ 11标准模板(STL) std::map(六)

定义于头文件<map> template< class Key, class T, class Compare std::less<Key>, class Allocator std::allocator<std::pair<const Key, T> > > class map;(1)namespace pmr { template <class Key, class T, clas…

优化器| SGD/SGD-m/SGD-NAG/Adagrad/Adadelta/RMSProp/Adam/Nadam/Adamax

前言&#xff1a;最近准备复习一下深度学习的基础知识&#xff0c;开个专栏记录自己的学习笔记 各种SGD和Adam优化器整理 基本概念 优化&#xff1a;最大化或最小化目标函数&#xff0c;具体指最小化代价函数或损失函数 损失函数 J(θ)f(hθ(x)&#xff0c;y)&#xff0c;h…

软考A计划-试题模拟含答案解析-卷五

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

Android 12.0仿ios的hotseat效果修改hotseat样式

1.概述 最近在12.0产品项目需求的需要,系统原生Launcher的布局样式很一般,所以需要重新设计ui对布局样式做调整,产品在看到 ios的hotseat效果觉得特别美观,所以要仿ios一样不需要横屏铺满的效果 居中显示就行了,所以就要看hotseat的具体布局显示了 效果图如下: 2.仿io…

《Spring Guides系列学习》guide51 - guide55

要想全面快速学习Spring的内容&#xff0c;最好的方法肯定是先去Spring官网去查阅文档&#xff0c;在Spring官网中找到了适合新手了解的官网Guides&#xff0c;一共68篇&#xff0c;打算全部过一遍&#xff0c;能尽量全面的了解Spring框架的每个特性和功能。 接着上篇看过的gui…

网络设备的部署(串行与并行)

串行设备 1.防火墙&#xff1a;能够实现区域隔离和访问控制 2.IPS(入侵防御系统)&#xff1a;能够检测入侵行为并阻断 3.WAF&#xff08;上网行为管理设备&#xff09;&#xff1a;保障web应用的安全 4.上网行为管理设备&#xff1a;对用户上网行为进行控制 5.FC交换机&am…