【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容器的底层结构,是一个经典的带头双向循环链表。每个节点包含:
- 数据
- 指向前一个节点的指针
- 指向后一个节点的指针。
这种结构赋予了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文档介绍
- 插入与删除:List的插入和删除操作非常高效,它可以在任意位置快速地添加或移除元素,而不需要像连续内存容器那样进行大量元素的移动。
- 多种构造:类都应该包含多种构造函数
- 支持迭代器:迭代器是C++重要的特性,我们写的list 也一定要支持迭代器。
2 框架搭建
现在我们开始实现list 容器,首先先要思考一下框架结构:
- 首先我们需要一个节点结构体(类似源码中的节点)
- 其次我们的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♪(・ω・)ノ谢谢阅读!!!
下一篇文章见!!!
相关文章:
【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…...
LinkedList用法详解(Java)
LinkedList LinkedList 是 Java 中的一个常用类,它实现了 List 接口,采用双向链表数据结构。 1. 创建 LinkedList 对象 import java.util.LinkedList;LinkedList<String> linkedList new LinkedList<>();2. 添加元素 linkedList.add(&q…...
34. 在排序数组中查找元素的第一个和最后一个位置
Problem: 34. 在排序数组中查找元素的第一个和最后一个位置 文章目录 思路解题方法复杂度Code 思路 二分查找, 口诀:左右右,求左段区间的右端点,动r 解题方法 两次二分查找 复杂度 时间复杂度: O ( l o g n ) O(logn) O(logn) 二…...
音乐文件逆向破解
背景 网易云等在线音乐文件的加密源码都按照一定的规则加密,通过对音乐文件的源码分析转化,有望实现对加密文件的解密 实现内容 实现对加密音乐文件的解密 实现对无版权的音乐文件的转化 实现环境 010editor 010 Editor是一个专业的文本编辑器和十六…...
xhci 数据结构
xhci 数据结构 xhci 数据结构主要在手册上有详细的定义,本文根据手册进行归纳总结: 重点关注的包括: device contexttrb ringtrb device context设备上下文 设备上下文数据结构由xHC管理,用于向系统软件报告设备配置和状态信息。…...
Go——Goroutine介绍
一. 并发介绍 进程和线程 进程是程序在操作系统中一次执行过程,系统进程资源分配和调度的一个独立单位。线程是进程执行的实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程可以创建和撤销多个线程,…...
Centos7,部署etcd集群,基于二进制包,https安全通讯
由于etcd集群https通讯,所以需要自建CA数字证书,学习使用https部署etcd集群前,可以先完成一下,基于http通信的etcd集群: 关于CA原理以及工作可以阅读,以下两篇文章: CA工作原理 对称加密与非对…...
设置MariaDB,创建新库,新用户并授权其可以从任何主机登录
OS:CENTOS 7 1、从系统进入MariaDB # mysql -u root -p 这里的root是指MariaDB的管理员用户,和系统的root不搭边,只是同名而已。 2、看下有哪些库、用户 MariaDB [(none)]> show databases; MariaDB [(none)]>select user,host from mysql.us…...
每日一VUE——组件的生命周期
文章目录 VUE组件的生命周期生命周期钩子函数实例创建Teleport VUE组件的生命周期 组件生命周期 组件从创建到挂载、更新、到销毁的一系列过程被称为组件的生命周期。 生命周期函数 在组件的各个生命周期节点执行的函数,为生命周期钩子函数。 生命周期钩子函数…...
Redis中的BigKey
Redis中的BigKey 文章目录 Redis中的BigKey什么是BigKey?BigKey的危害找到Bigkey删除BigKey优化BigKeyBigKey对持久化的影响对AOF日志的影响对AOF重写和RDB的影响 什么是BigKey? 大 key 并不是指 key 的值很大,而是 key 对应的 value 很大。…...
MySQL中的存储过程详解(上篇)
使用语言 MySQL 使用工具 Navicat Premium 16 代码能力快速提升小方法,看完代码自己敲一遍,十分有用 拖动表名到查询文件中就可以直接把名字拉进来中括号,就代表可写可不写 目录 1.认识存储过程 1.1 存储过程的作用 1.2 存储过程简介…...
面试官:说一说CyclicBarrier的妙用!我:这个没用过...
写在开头 面试官:同学,AQS的原理知道吗? 我:学过一点,抽象队列同步器,Java中很多同步工具都是基于它的… 面试官:好的,那其中CyclicBarrier学过吗?讲一讲它的妙用吧 我&…...
MySQL高可用搭建方案MHA
MHA架构介绍 MHA是Master High Availability的缩写,它是目前MySQL高可用方面的一个相对成熟的解决方案,其核心是使用perl语言编写的一组脚本,是一套优秀的作为MySQL高可用性环境下故障切换和主从提升的高可用软件。在MySQL故障切换过程中&am…...
【vue】用vite创建vue项目
前置要求 要有Node.js 1. 用vite创建vue项目 在cmd中,进入一个文件夹 在文件资源管理器上面的文件目录中,输入cmd,回车在cmd中通过cd命令进入对应文件夹 创建项目 npm create vitelatest # 创建项目创建项目过程中的一些选项 Ok to pro…...
内网渗透-内网环境下的横向移动总结
内网环境下的横向移动总结 文章目录 内网环境下的横向移动总结前言横向移动威胁 威胁密码安全 威胁主机安全 威胁信息安全横向移动威胁的特点 利用psexec 利用psexec.exe工具msf中的psexec 利用windows服务 sc命令 1.与靶机建立ipc连接2.拷贝exe到主机系统上3.在靶机上创建一个…...
Linux命令学习—linux 的常用命令
1.1、改变目录 cd 目录的表达方法: /根目录 .当前目录 .. 上一级目录 ~家目录 #cd / 进入到系统根目录 #cd . 进入当前目录 #cd .. 进入当前目录的父目录,返回上层目录 #cd /tmp 进入指定目录/tmp #cd ~ 进入当前用户的家目录 #cd …...
【Git教程】(十)版本库之间的依赖 —— 项目与子模块之间的依赖、与子树之间的依赖 ~
Git教程 版本库之间的依赖 1️⃣ 与子模块之间的依赖2️⃣ 与子树之间的依赖🌾 总结 在 Git 中,版本库是发行单位,代表的是一个版本,而分支或标签则只能被创建在版本库这个整体中。如果一个项目中包含了若干个子项目,…...
最新版IntelliJ IDEA 2024.1安装和配置教程 详细图文解说版安装教程
IntelliJ IDEA 2024.1 最新版如何快速入门体验?IntelliJ IDEA 2024.1 安装和配置教程 图文解说版 文章目录 IntelliJ IDEA 2024.1 最新版如何快速入门体验?IntelliJ IDEA 2024.1 安装和配置教程 图文解说版前言 第一步: IntelliJ IDEA 2024.1安装教程第 0 步&…...
JVM常用参数一
jvm启动参数 JVM(Java虚拟机)的启动参数是在启动JVM时可以设置的一些命令行参数。这些参数用于指定JVM的运行环境、内存分配、垃圾回收器以及其他选项。以下是一些常见的JVM启动参数: -Xms:设置JVM的初始堆大小。 -Xmx࿱…...
分布式锁-redission可重入锁原理
5.3 分布式锁-redission可重入锁原理 在Lock锁中,他是借助于底层的一个voaltile的一个state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state0,假如有人持有这把锁,那么state1,如果持有这把锁的…...
Android Gradle开发与应用 (八) :Kotlin DSL
1. 前言 本文介绍了Gradle Kotlin DSL相关的一些知识点 2. DSL是什么 DSL是为特定领域设计的专门的语言,也就是设计了一门语言,然后解决某个特定的领域的特定问题。 2.1 举例说明 以下的这些都可以称之为DSL 正则表达式 :用于文本处理的特定语言SQ…...
phpstorm 快捷键
PHPstorm最常用的快捷键,提高开发效率 - 知乎 (zhihu.com) 四年精华PHP技术文章整理合集——PHP框架篇 (qq.com) 四年精华PHP技术文合集——微服务架构篇 (qq.com) Vue3 打印票据 预览的库:vue3打印解决方案:Vue-Plugin-HiPrint - 掘金 (j…...
浦大喜奔APP8.0智能升级,发力数字金融深化五大金融篇章服务
1. 浦大喜奔立足科技赋能持续迭代升级,筑牢用户体验护城河 浦发信用卡中心坚持数字科技与客户体验双轮驱动,以科技赋能发展,优化整体系统性能,全方位支撑浦大喜奔 APP提高线上客户服务能力与体验,积极服务民生消费&a…...
自然语言处理、大语言模型相关名词整理
自然语言处理相关名词整理 零样本学习(zero-shot learning)词嵌入(Embedding)为什么 Embedding 搜索比基于词频搜索效果好? Word2VecTransformer检索增强生成(RAG)幻觉采样温度Top-kTop-p奖励模…...
移动开发避坑指南——内存泄漏
在日常编写代码时难免会遇到各种各样的问题和坑,这些问题可能会影响我们的开发效率和代码质量,因此我们需要不断总结和学习,以避免这些问题的出现。接下来我们将围绕移动开发中常见问题做出总结,以提高大家的开发质量。本系列文章…...
太好玩了,我用 Python 做了一个 ChatGPT 机器人
毫无疑问,ChatGPT 已经是当下编程圈最火的话题之一,它不仅能够回答各类问题,甚至还能执行代码! 或者是变成一只猫 因为它实在是太好玩,我使用Python将ChatGPT改造,可以实现在命令行或者Python代码中调用。…...
STM32存储左右互搏 SDIO总线读写SD/MicroSD/TF卡
STM32存储左右互搏 SDIO总线读写SD/MicroSD/TF卡 SD/MicroSD/TF卡是基于FLASH的一种常见非易失存储单元,由接口协议电路和FLASH构成。市面上由不同尺寸和不同容量的卡,手机领域用的TF卡实际就是MicroSD卡,尺寸比SD卡小,而电路和协…...
累积分布函数图(CDF)的介绍、matlab的CDF图绘制方法(附源代码)
在对比如下两个误差的时候,怎么直观地分辨出来谁的误差更低一点?: 通过这种误差时序图往往不容易看出来。 但是如果使用CDF图像,以误差绝对值作为横轴,以横轴所示误差对应的累积概率为纵轴,绘制曲线图&am…...
代码随想录算法训练营第四十一天|343.整数拆分、96不同的二叉搜索树
文档链接:https://programmercarl.com/ LeetCode343.整数拆分 题目链接:https://leetcode.cn/problems/integer-break/ 思路: j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘…...
全量知识系统 程序详细设计之 统一资产模型(QA-SmartChat)
Q1. 下面我们聊聊整个全知系统的设计 的矩阵和函数,矩阵表示的是“活物”,分别 类似 一个基因的活性、一个实体的辨识度和某种特征的可区分度。 函数的可微、可积和可导性 则表示 运动的控制方式 在全知系统设计中,矩阵和函数是两个核心的组…...
iis建设的网站无法访问/seo概念的理解
MySQL联合索引VS单列索引以一个一千万数据量的表格为例mysql1. 建表建索引USE foo;DROP TABLE IF EXISTS tmp;CREATE TABLE tmp (id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,school_id INT UNSIGNED NOT NULL,student_id INT UNSIGNED NOT NULL,INDEX school_id(school_id),I…...
在线手机网站建设/站长网站工具
detectjQuery代码段,用于在启用条款和条件复选框之前检测用户是否已滚动到页面底部(或带滚动的div)。 Terms of service jargon stuff hereI accept the blah, blah, blah.jQuery(document).ready(function() {jQuery("input#TERMS_ACC…...
wordpress 怎么添加即时联系窗口/网店运营流程步骤
原标题:听说,近视的人智商更高?近视的你还好吗如果你在学校里走路的时候碰到了熟人跟他打招呼却没有得到回应千万不要生气因为他可能是因为……………………没戴眼镜!!!近日,德国美因茨大学科学…...
安徽建站费用/域名注册查询网站
一台linux 服务器(没有光驱)出现故障,导致无法进入系统,该怎么办呢? 怎样把里面受损的文件给它替换掉呢? 下面我将要详细的讲一下如何对它进行故障恢复。 (一) 制作引导U盘。把系统引…...
申请一个免费的网站空间/代推广app下载
前段时间,结合Andriod手机做了UDP的C/S通信,简单传送字符串,还有自定义UDP通信协议,作了传送火车票的信息,并进行反馈。 UDP通信:理解几个名词 1.DatagramSocket:用来发送和接收数据包的套接字…...
太原视频剪辑培训机构哪个好/台州网站seo
一、迭代器 JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就需要一种统一的接口机制,来处理所有不同的数据结构。遍历器(I…...