【C++】手搓 list 容器

在这里插入图片描述
送给大家一句话:

若结局非你所愿,就在尘埃落定前奋力一搏。—— 《夏目友人帐》

手搓 list 容器

  • 1 前言
    • 1.1 底层结构
    • 1.2 使用场景
    • 1.3 功能简介
  • 2 框架搭建
    • 2.1 节点类
    • 2.2 list 类
    • 2.3 迭代器类
  • 3 功能实现
    • 3.1 begin() 与 end()
    • 3.2 插入操作
    • 3.3 删除操作
    • 3.4 拷贝构造
    • 3.5 析构函数
    • 3.6 其他函数
  • 4 总结
  • Thanks♪(・ω・)ノ谢谢阅读!!!
  • 下一篇文章见!!!

1 前言

List是C++标准模板库(STL)中的一个成员,其本质为带头双向循环链表。不同于连续的、紧密排列的数组容器Vector,List容器的内部是由双向链表构成的,使得它在插入和删除操作上,就如同行云流水一般顺畅,不需移动其它元素。

1.1 底层结构

List容器的底层结构,是一个经典的带头双向循环链表。每个节点包含:

  1. 数据
  2. 指向前一个节点的指针
  3. 指向后一个节点的指针。

这种结构赋予了List灵动的特性:它能够轻松地在任意位置增加或移除元素,而这种操作几乎是与容器大小无关的,体现了时间复杂度上的优势。但这种优势的代价是,与数组相比,List在访问元素时的速度会较慢,因为它需要从头开始遍历。这也决定了list的更适合频繁插入的动态数据。
来看STL源码中的节点结构:

template <class T>
struct __list_node {typedef void* void_pointer;void_pointer next;void_pointer prev;T data;
};

1.2 使用场景

List最适合的场景是那些需要频繁进行插入和删除操作的场合。
例如,如果你正在管理一个动态变化的列表,如任务调度、人员排队等场景,List的特性将大放异彩。但是如果你的应用场景更多地需要随机访问元素,那么向量(Vector)或者数组可能是更佳的选择。因为List的顺序访问性能相比之下会显得有些力不从心。

  • 所以如果需要频繁随机的访问数据,那么选择vector容器
  • 如果需要频繁插入删除数据,那么选择list容器
  • 排序不要选择list !!!其删除节点的过程就决定了其速度不会太快。

1.3 功能简介

功能简介我们可以参考STL官方库 :list文档介绍

  1. 插入与删除:List的插入和删除操作非常高效,它可以在任意位置快速地添加或移除元素,而不需要像连续内存容器那样进行大量元素的移动。
  2. 多种构造:类都应该包含多种构造函数
  3. 支持迭代器:迭代器是C++重要的特性,我们写的list 也一定要支持迭代器。

2 框架搭建

现在我们开始实现list 容器,首先先要思考一下框架结构:

  1. 首先我们需要一个节点结构体(类似源码中的节点)
  2. 其次我们的list 类要带一个头结点,让我们更方便进行插入删除操作

基本就是这样,现在我们开始手搓

2.1 节点类

// 节点 结构体
template<class T>
struct ListNode
{ListNode* _next;ListNode* _prev;T _data;ListNode(T x = T()) :_next(nullptr),_prev(nullptr),_data(x){}//ListNode(T x = T()) //{//	_next = nullptr;//	_prev = nullptr;//	_data = x;//}	~ListNode(){_next = nullptr;_prev = nullptr;}
};

这里使用模版来适配更多的情景,然后基本的数据,前后指针都很简单。在编写一个构造函数,==构造函数使用初始化列表,并不是必须使用。析构函数避免野指针出现,将指针赋值为nullptr就可以了。

2.2 list 类

我们先进行简单的框架书写,构造函数需要创建一个头结点,因为我们要创建双向循环链表,所以头尾都要指向头结点本身。 _size赋初值。

template<class T>
class list
{
public://设置适配的节点typedef ListNode<T> Node;//空初始化void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}//构造函数list() :_head(nullptr){empty_init();}
private:Node* _head;size_t _size;
};

接下来我们来逐步完成功能书写,由于我们还没有进行迭代器的书写

2.3 迭代器类

我们思考一下这里能不能使用原生指针来完成迭代器的操作(++ == != --)当然是不能的,因为链表的物理地址并不是连续的,对原生指针的++或–操作是没有意义的,所以我们需要自行编写迭代器类,对原生指针进行封装,来满足我们特殊的++和–操作。

	//这里的模板可以再次升级  先简单写一下template<class T>class ListIterator {public://重命名方便书写typedef ListNode<T> Node;typedef ListIterator<T> Self;Node* _node;ListIterator(Node* x ):_node(x){}//操作符重载 前置++ 与 后置++的区别是参数是否带(int)//++tSelf operator++(){_node = _node->_next;return *this;}//t++Self operator++(int){Self tmp(*this);//浅拷贝即可_node = _node->_next;return tmp;}//--tSelf operator--(){_node = _node->_prev;return *this;}//t--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;}// 解引用操作 *it 返回节点数据的引用 可以进行修改T& operator*(){return  _node->_data;}//因为指针才能使用-> 所以->要返回地址(指针)		T* operator->()//编译器会进行省略->{return &_node->_data;}};

这样迭代器类就大致写好了,那么一般我们的迭代器应该还要支持const,不然我们传入一个不可修改的链表(const list l),就会反生报错,那么我们还要书写一份const版的迭代器。如果进行编写,那么是不是会有大部分与刚才我们书写的迭代器重复(++ -- == != 都是一样的),只有operator*()operator->()返回值不一致:

  • 因为是常迭代器,使用场景是对const list<T> l进行操作,那么节点数据不可改变,所以不影响++ -- == != 这些操作,受影响的是operator*()operator->()返回值(该情况下链表本身是只读的,又因为不能将权限进行扩大,所以返回值应该也是只读的(const))。
  • 那这样就发现了不同常迭代器应该为 const T& operator*()const T* operator->() ,所以有没有一种办法可以简单解决呢,当然有了,我们设置一个新模版(带有三个参数),创建的时候就传入对应参数

我们将模版修改为这样,

 //reference 引用  pointer 指针
template<class T , class Ref ,class Ptr>

对应返回值也改变:

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

那么类实例化的时候传入对应参数就好了:

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

这样就实现了迭代器的创建,是不是就非常简洁了呢

3 功能实现

3.1 begin() 与 end()

使用迭代器即可,注意end()是头结点,因为遍历过程中,全部遍历后会回到头结点,所以直接判断是否为头结点就能控制结束位置。

//普通迭代器
typedef ListIterator<T, T&, T*> iterator;
//常迭代器
typedef ListIterator<T, const T&, const T*> const_iterator;iterator begin() { return _head->_next; }
iterator end() { return _head; }const_iterator begin() const { return _head->_next; }
const_iterator end() const { return _head; }

3.2 插入操作

插入操作我们很熟悉,步骤是创建一个新节点,然后通过改变指针指向来完成插入操作:
来看尾插操作,

void push_back(const T& x = T())
{//创建新节点Node* node = new Node(x);//找尾Node* tail = _head->_prev;//进行插入node->_next = _head;node->_prev = tail;tail->_next = node;_head->_prev = node;//别忘记大小++_size++;
}

任意位置插入,操作思路依然是对前后节点与新节点的指针指向进行操作,来完成插入。

void insert(iterator pos = begin(), T x = T())
{//创建新节点Node* node = new Node(x);//前节点 后节点Node* prev = pos._node->_prev;Node* next = pos._node;//处理新节点node->_prev = prev;node->_next = next;//出现前后节点prev->_next = node;next->_prev = node;//别忘记大小++_size++;
}		

头插,直接干脆调用insert就可以了

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

3.3 删除操作

删除操作,同样是使用指针操作,来达到删除的效果。注意要对删除的节点进行释放空间操作(delete),不然会发生内存泄漏!!!

尾删
void pop_back()
{Node* tail = _head->_prev;Node* prev = tail->_prev;prev->_next = _head;_head->_prev = prev;delete tail;_size--;
}
//头删
void pop_front()
{Node* head = _head->_next;Node* next = head->_next;_head->_next = next;next->_prev = _head;delete head;_size--;
}
//任意位置删除
iterator erase(iterator pos)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;_size--;return iterator(next);
}

需要注意的是,任意位置删除因为使用了迭代器,删除后会造成迭代器失效,所以需要更新迭代器,返回被删除节点的下一个节点的迭代器即可。

3.4 拷贝构造

拷贝构造直接将数据一个一个插入到该链表中即可:

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

这样十分方便快捷!!!

3.5 析构函数

void clear()
{//依次释放iterator it = begin();while (it != end()){it = erase(it);}
}
~list()
{clear();//需要单独释放头结点空间delete _head;_head = nullptr;
}

3.6 其他函数

返回大小:

size_t size() const { return _size; }

判断是否为空:

bool empty()
{return _size == 0;
}

清空数据:

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

4 总结

本文我们实现了STL库中重要的list 的模拟实现,其中最重要莫过于迭代器的封装类的书写,这是前所未有的操作(对于我来说,我是第一次使用这种结构)。通过list 的模拟实现也帮我们巩固了类与对象的知识,也强化了指针操作的思路。欢迎大家讨论分析。

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

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

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

相关文章

ATM04-6P 安费诺汽车连接器6芯压线端子胶壳

ATM04-6P是一款压线端子胶壳&#xff0c;属于Amphenol&#xff08;安费诺&#xff09;品牌 ATM04-6P 规格信息&#xff1a; 制造商:Amphenol 产品种类:汽车连接器 RoHS:是 产品:Connectors 位置数量:6 Position 型式:Receptacle (Female) 线规量程:22 AWG to 16 AWG 系列:ATM 颜…

ssm032基于Java的汽车客运站管理系统的设计与实现+jsp

汽车客运站管理系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对汽车客运站售票信息管理混乱&…

layui后台框架,将左侧功能栏目 集中到一个页面,通过上面的tab切换 在iframe加载对应页面

实现上面的 功能效果。 1 html代码 <form class"layui-form layui-form-pane" action""><div class"layui-tab" lay-filter"demo"><ul class"layui-tab-title"><li id"a0" class"lay…

使用 proxySQL 来代理 Mysql

我有若干台云主机&#xff0c; 但是只有1个台vm 具有外部ip 而在另1台vm上我安装了1个mysql instance, 正常来讲&#xff0c; 我在家里的电脑是无法连接上这个mysql 尝试过用nginx 代理&#xff0c; 但是nginx只能代理http协议的&#xff0c; mysql 3306 并不是http协议 解决…

Leetcode 6. Z 字形变换

将一个给定字符串 s 根据给定的行数 numRows &#xff0c;以从上往下、从左到右进行 Z 字形排列。 比如输入字符串为 “PAYPALISHIRING” 行数为 3 时&#xff0c;排列如下&#xff1a; P A H N A P L S I I G Y I R 之后&#xff0c;你的输出需要从左往右逐行读取&#xff0…

BM96 主持人调度(二)(贪心算法)

一开始写的时候忘了给start、end数组赋值了 import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可** 计算成功举办活动需要多少名主持人* param n int整型 有n个活动* param start…

list介绍及使用

文章目录 list的介绍及使用1. list的介绍2. list的使用2.1 list的构造2.2 list iterator的使用2.3 list capacity2.4 list element access2.5 list modifiers2.6 list的迭代器失效 list的介绍及使用 1. list的介绍 list文档介绍 list是可以在 常数范围内 在任意位置进行插入和…

Ubuntu22.04修改默认窗口系统为X11

Ubuntu22.04安装默认窗口系统为Wayland&#xff08;通过设置->关于可以看到&#xff09;。 一、用Ubuntu on Xorg会话登录 用户登录时&#xff0c;点“未列出”&#xff0c;输入用户名后&#xff0c;在登录界面底部的齿轮图标中&#xff0c;选择 "Ubuntu on Xorg&quo…

Druid报错:Error attempting to get column ‘DISABLE_DATE‘ from result set.

背景 Mybatis-Plus 3.2.0 Druid 1.1.20 Oracle 11g 版本在处理 LocalDateTime 时报错&#xff1a; org.springframework.dao.InvalidDataAccessApiUsageException: Error attempting to get column ‘DISABLE_DATE’ from result set. Cause: java.sql.SQLFeatureNotSupport…

备考ICA----Istio实验18---单集群中部署多个Istio控制面

备考ICA----Istio实验18—单集群中部署多个Istio控制面 单个 Kubernetes 控制面以及多个 Istio 控制面和多个网格。通过 Kubernetes 命名空间和 RBAC 实现软多租户业务隔离。 1. 环境准备 1.1 创建2个命名空间 kubectl create ns usergroup-1 kubectl label ns usergroup-…

STC89C52学习笔记(六)

STC89C52学习笔记&#xff08;六&#xff09; 综述&#xff1a;本文讲述了51单片机的定时器和中断&#xff0c;还讲述了如何初始化定时器、编写中断服务函数和完成定时器控制LED闪烁。 一、定时器 1. 作用 ①用于计时 ②替代长时间的Delay。因为在Delay下&#xff0c;单片…

反转链表 II(leetcode)

题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 分享两道解题思路&#xff1a; 第一个&#xff1a; 将left~right之间的节点翻转&#xff0c;首先left前的节点的next置为空&#xff0c;right的指针置为空&#xff0c; 然后拼接 p1指的是left前面的一个 p1-…

使用新版FLIR (FLIR_ADAS_v2) 数据集创建yolo格式数据集(目标检测)

FLIR在2022.1.19发布了新版的FLIR_ADAS_v2&#xff0c;有着更多的类别和数量更丰富的图像。数据集同步注释热图像和无注释RGB图像供参考。本文章主要介绍如何使用FLIR_ADAS_v2中的rgb图像和thermal图像来制作yolo格式数据集。 1.官方数据集下载&#xff1a;FLIR_ADAS_v2数据集…

win11 连接海康摄像头 ONVif协议

目录 Win 11 通过脚本打开自带的IE浏览器访问海康摄像头 海康摄像头设置支持onvif协议 安装onvif协议 onvif协议示例代码 Win 11 通过脚本打开自带的IE浏览器访问海康摄像头 第一步、桌面右键新建一个 txt 的文档 第二步、打开文档并且复制粘贴下面代码 CreateObject(&…

OpenCV单通道图像按像素成倍比例放大(无高斯平滑处理)

OpenCV中的resize函数可以对图像做任意比例的放大(/缩小)处理&#xff0c;该处理过程会对图像做高斯模糊化以保证图像在进行放大&#xff08;/缩小&#xff09;后尽可能保留源图像所展现的具体内容&#xff08;消除固定频率插值/采样带来的香农采样信息损失&#xff09;&#x…

【深度学习】环境搭建ubuntu22.04

清华官网的conda源 https://mirrors.tuna.tsinghua.edu.cn/help/anaconda/ 安装torch conda install pytorch torchvision torchaudio pytorch-cuda12.1 -c pytorch -c nvidia 2.2.2 conda install 指引看这里&#xff1a; ref:https://docs.nvidia.com/cuda/cuda-installatio…

Leetcode面试经典150_Q14最长公共前缀

题目&#xff1a; 编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀&#xff0c;返回空字符串 ""。 思路A&#xff1a;横向/纵向扫描 Python&#xff1a; class Solution:def longestCommonPrefix(self, strs: List[str]) -> str:s "…

微软edge浏览器上网、下载速度慢,如何解决??

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

【Spring】一问详解什么是Spring IoC和DI

目录 一、IoC & DI入门1.1、Spring1.1.1、什么是容器1.1.2、什么是IoC 1.2、IoC介绍1.2.1、传统程序开发1.2.2、问题分析1.2.3、问题解决1.2.4、 IoC优势 1.3、Bean的作用域1.4、DI介绍 二、IoC详解2.1、Bean的存储2.1.1、类注解的使用2.1.2、获取bean对象的其他方式2.1.3、…

【Linux 命令】内核、驱动调试手段总结

文章目录 1. printk2. strace3. Itrace4. ptrace5. ftrace6. 动态打印7. perf8. devmem9. demsg参考&#xff1a; 1. printk **printk()**是 Linux 内核中最广为人知的函数之一。它是我们打印消息的标准工具&#xff0c;通常也是追踪和调试的最基本方法。 虽然 printk() 是基…