【C++】list原理讲解及其实现

目录
一、认识list底层结构
二、list的构造类函数
三、迭代器
四、数据的访问
五、容量相关的函数
六、关于数据的增删查改操作
七、list和vector的比较

前言

要模拟实现list,必须要熟悉list的底层结构以及其接口的含义,在上一篇我们仔细讲解了list的常见接口的使用及其含义,这篇我们就直接进入主题


一、list底层结构

list底层实现的是带头双向循环链表,list底层实现需要三个类,分别是链表的结点类,链表的迭代器类链表本身

// List的节点类
template<class T>
struct ListNode
{ListNode<T>* _prev;ListNode<T>* _next;T _data;ListNode(const T& data=T()):_prev(nullptr),_next(nullptr),_data(data){}
};
//List的迭代器类
template<class T,class Ref,class Ptr>
struct ListIterator
{typedef ListNode<T> Node;typedef ListIterator<T,Ref,Ptr> Self;Node* _node;ListIterator(Node* node):_node(node){}Ref operator*();Ptr operator->();Self& operator++();Self operator++(int);Self& operator--();Self operator--(int);bool operator!=(const Self& l);bool operator==(const Self& l);
};
 //list类
template<class T>
class list
{typedef ListNode<T> Node;Node* _head;//哨兵位的头节点size_t _size;//链表数据个数
public:typedef ListIterator<T, T&, T*> iterator;typedef ListIterator<T, const T&, const T&> const_iterator;
}

给大家讲一下这三个类的关联,链表类是主类,链表的结点用一个类封装起来,构造起来更方便,放在主类里面不方便且冗余,由于链表的物理结构不连续,所以不能像vector和string那样单纯的用原生指针来实现,我们可以把结点类再次进行封装,封装成迭代器,让它能很好的指向链表的结点,利用它的结构优势来重载运算符遍历这个链表


二、初始化list的构造函数

1、默认构造
list();

void empty_init()
{_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;
}list()
{empty_init();//这里没有直接写默认构造,而是通过empty_init来实现,因为后面我们要多次用到这个函数来初始化哨兵位的头节点,写在默认构造里面就不方便其他地方的调用了
}

2、用n个val来构造链表
list(size_t n, const T& val = T());

list(size_t n, const T& val = T())
{empty_init();//要记得初始化哨兵位的头节点for (size_t i = 0; i < n; i++){push_back(val);}_size = n;
}

3、迭代器区间构造
template <class InputIterator> list(InputIteratorfirst, InputIterator last);

list(InputIterator first, InputIterator last)
{empty_init();while (first != last){push_back(*first);first++;}
}

特别注意:
如果你写了迭代器区间构造,就一定要重载上面的list(size_t n, const T& val = T()),如果你不重载的话,他会报一个错误 非法间接寻址,博主我深受其害🤡,再重载一个这个函数list(int n, const T& val = T());就可以 其实就是改一个类型的事

为什么要重载一下?

list<int> li(10,1);
你传的参数都是int类型,两个类型是一样的,而迭代器区间构造的类型是一样的,最符合,这就导致你没走你
想走的那个构造函数,你想走的那个函数list(size_t n, const T& val = T())在这个案例中是size_t和int类型,虽然
也可以走这个函数,但是编译器觉得迭代器区间构造更好,所以你得重载一个int,int类型的,这样编译器就会
走你重载的函数了

4、拷贝构造
(用来初始化一个正在创建的对象)
list(const list<T>& li);

list(const list<T>& li)
{empty_init();for (auto& e : li){push_back(e);}_size = li._size;
}

5、赋值构造
(两个已经存在的对象,一个赋值给另一个)
list<T>& operator=(const list<T> li)

void swap(list<T>& li)
{std::swap(_head, li._head);std::swap(_size, li._size);
}
//这里的赋值构造,我用的是现代写法
list<T>& operator=(list<T> li)//这里的参数使用传值传参,会调用拷贝构造,
{swap(li);//直接让*this和临时对象li交换,出了作用域li也就销毁了return *this;
}

6、析构函数
~list();

//clear只需要清除掉里面的数据就行,也就是析构掉除哨兵位以外的结点
void clear()
{iterator it = begin();while (it != end()){it = erase(it);}_size = 0;
}
~list()
{clear();delete _head;_head = nullptr;
}

三、迭代器

讲到list迭代器这里,我们先把迭代器类完善一下

//这里弄了三个模板参数,好实例化生成iterator和const_iterator,因为他们两个基本上没什么区别,只用区分好解引用后的返回值类型就好
template<class T,class Ref,class Ptr>//Ref表示引用,Ptr表示指针
struct ListIterator
{typedef ListNode<T> Node;typedef ListIterator<T,Ref,Ptr> Self;Node* _node;ListIterator(Node* node):_node(node){}//*itRef operator*(){return _node->_data;}//it->Ptr operator->(){return &_node->_data;}//++iSelf& operator++(){_node = _node->_next;return *this;//返回的变量出了作用域不会被销毁用引用返回更合适}//i++Self operator++(int){Self temp(*this);_node = _node->_next;return temp;//返回的变量出了作用域会被销毁用传值返回更合适}Self& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self temp(*this);_node = _node->_prev;return temp;}bool operator!=(const Self& s){return _node != s._node;//这里是用迭代器的地址去比较}bool operator==(const Self& s){return _node == s._node;}
};

三个模板参数的主要意图:
在这里插入图片描述

这个类里面的接口,我们重点讲解一下这个Ptr operator->();如下图:
在这里插入图片描述

iterator begin();
iterator end();
const_iterator begin();
const_iterator end();
在这里插入图片描述
begin表示链表的第一个结点,第一个结点应该是哨兵位的下一个结点,哨兵位是不存放任何东西的,单纯作为头结点,end表示最后一个结点的下一个位置,list是一个带头双向循环链表,所以最后一个结点的下一个位置就是_head,迭代器遍历的区间[begin,end) ,左闭右开区间,这样就能遍历到所有结点

template<class T>
class list
{typedef ListNode<T> Node;Node* _head;//哨兵位的头节点size_t _size;//链表数据个数
public:typedef ListIterator<T, T&, T*> iterator;typedef ListIterator<T, const T&, const T&> const_iterator;iterator begin(){//return iterator(_head->_next);匿名对象return _head->_next;//单参数的构造函数隐式类型转换}iterator end(){return _head;}const_iterator begin(){return _head->_next;}const_iterator end(){return _head;}
}

四、数据的访问

1、front 访问头结点
T& front();

T& front()
{return _head->_next->_data;
}

const T& front()const;

const T& front() const
{return _head->_next->_data;
}

2、back 访问尾节点
T& back();

T& back()
{return _head->_prev->_data;
}

const T& back()const;

const T& back() const
{return _head->_prev->_data;
}

五、容量相关的函数

size 有效数据个数
size_t size()const;

size_t size() const 
{return _size;
}

empty 判断是否为空
bool empty()const;

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

六、关于数据的增删查改操作

push_back 尾插数据
void push_back(const T& val) ;

void push_back(const T& val)
{Node* newnode = new Node(val);//为插入的数据new一个结点出来Node* tail = _head->_prev;//先保存链表尾部的结点//处理好tail和newnode的指向tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;//将newnode更新成为新的尾节点_size++;
}

pop_back 尾删数据
void pop_back() ;

void pop_back()
{Node* tail = _head->_prev;Node* prev = tail->_prev;delete tail;prev->_next = _head;_head->_prev = prev;_size--;
}

push_front 头插数据
void push_front(const T& val);

void push_front(const T& val)
{Node* newnode = new Node(val);Node* next = _head->_next;//_head newnode nextnewnode->_next = next;next->_prev = newnode;newnode->_prev = _head;_head->_next = newnode;_size++;
}

pop_front 头删数据
pop_front();

void pop_front()
{Node* del = _head->_next;Node* next = del->_next;delete del;_head->_next = next;next->_prev = _head;_size--;
}

insert 在pos位置前插入值为val的节点
iterator insert(iterator pos, const T& val);

void  insert(iterator pos, const T& val)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(val);//prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;_size++;}

erase 删除pos位置的节点,返回该节点的下一个位置
iterator erase(iterator pos);

iterator erase(iterator pos)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;delete cur;prev->_next = next;next->_prev = prev;_size--;return next;//这样写会发生隐式类型转换,转换成iterator类型//写成这样也可以iterator(next);匿名对象
}

clear 清除结点
void clear();

void clear()
{iterator it = begin();while (it != end()){it = erase(it);//这里需要注意一下,erase掉当前的结点后,会导致迭代器失效,erase会返回下一个结点的迭代器,删除之后接收一下就行}_size = 0;
}

swap 交换两个链表
void swap(list<T>& l);

void swap(list<T>& li)
{//利用库里面的swap交换两个链表的头结点和size就可以了std::swap(_head, li._head);std::swap(_size, li._size);
}

七、list和vector的区别

在这里插入图片描述


list篇到这里就结束了🎉,欢迎大家来指教我的下一篇stack和queue

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

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

相关文章

网络学习(二)|网络标识符URI与URL的区别与联系

文章目录 URIURL区别与联系示例结论 在工作中&#xff0c;我们经常会听到URI和URL这两个术语&#xff0c;它们是网络中不可或缺的概念。尽管它们常常被混淆使用&#xff0c;但它们代表着不同的概念和角色。我们将深入探讨URI和URL&#xff0c;帮助彻底理解它们之间的区别和联系…

Gradio 案例——将 dicom 文件转为 nii文件

文章目录 Gradio 案例——将 dicom 文件转为 nii文件界面截图依赖安装项目目录结构代码 Gradio 案例——将 dicom 文件转为 nii文件 利用 SimpleITK 库&#xff0c;将 dicom 文件转为 nii文件更完整、丰富的示例项目见 GitHub - AlionSSS/dcm2niix-webui: The web UI for dcm2…

Oracle 数据块之变化时的SCN

有很多操作会引起数据块改变&#xff0c;如业务数据的变化&#xff0c;块清理等。数据块变化时的SCN保存在数据块OFFSET 8-14中&#xff0c;占6个字节&#xff0c;如下所示&#xff1a;此外使用数据块DUMP命令也可以从跟踪文件中找到数据库的改变SCN。数据块的DUMP命令如下所示…

JavaScript精粹(一)- 基础入门

JavaScript&#xff08;简称为JS&#xff09;是一种广泛应用于网页开发的脚本语言&#xff0c;具有以下几个主要作用&#xff1a; 网页交互&#xff1a;JavaScript 可以用于创建动态的网页效果&#xff0c;例如响应用户的操作&#xff0c;实现页面内容的动态更新&#xff0c;以…

【Java基础】集合(2) —— List

List 存储的对象是有序的&#xff08;集合中存储对象的顺序和使用add方法添加对象的顺序一致&#xff09;&#xff0c;存储的对象是可重复的。 List的特有的功能: 都是可以操作索引的功能。 增: void add(int index, E element )boolean addAll(int index, Collection<? …

mysql的explain

explain可以用于select&#xff0c;delete&#xff0c;insert&#xff0c;update的statement。 当explain用于statement时&#xff0c;mysql将会给出其优化器&#xff08;optimizer&#xff09;的执行计划。 通过explain字段生成执行计划表。下面来解析这个执行计划表的每一列…

React Native 之 样式使用(三)

在 React Native 中&#xff0c;使用 JavaScript 来写样式。 所有的核心组件都接受名为style的属性。这些样式名基本上是遵循了web 上的 CSS 的命名&#xff0c;只是按照 JS的语法要求使用了驼峰命名法&#xff0c;例如将background-color改为backgroundColor。 建议使用StyleS…

提升用户体验:Xinstall免邀请码功能详解

在移动互联网时代&#xff0c;App的推广和运营显得尤为重要。然而&#xff0c;传统的App推广方式往往需要用户填写繁琐的邀请码&#xff0c;这不仅降低了用户体验&#xff0c;还影响了推广效果。幸运的是&#xff0c;Xinstall作为国内专业的App全渠道统计服务商&#xff0c;推出…

汽车电子都应用哪些频点的贴片晶振

晶振通过与其它元器件的连接使用&#xff0c;产生脉冲起到信号源的作用&#xff0c;所以在电子圈中有一个很形象的比喻: 如果把芯片比如电路的控制大脑&#xff0c;那晶振产生的信号就是给大脑持续供需的血液。平时&#xff0c;在我们使用的手机&#xff0c;蓝牙耳机&#xff0…

没有疯狂内卷的日本智能机市场,小屏与设计仍旧是主流

如果聊起国内的智能机市场&#xff0c;我想大多数人的印象就是疯狂内卷。卷影像、卷屏幕、卷快充、卷性能……客观地说&#xff0c;国内的3C产品还是很有质价比的。不过在没有如此内卷的日本市场&#xff0c;各种小屏手机仍旧是主流。 除了苹果外&#xff0c;日本本土品牌的夏普…

LeetCode算法题:两数之和

给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返回…

代码随想录算法训练营第40天|● 343. 整数拆分 ● 96.不同的二叉搜索树

343. 整数拆分 动归 递归拆 class Solution:def integerBreak(self, n: int) -> int:dp[0]*(n1)dp[2]1for i in range(3,n1):for j in range(1,i//21):dp[i]max(dp[i],(i-j)*j,dp[i-j]*j)return dp[n] 公式&#xff1a;全拆3&#xff0c;剩1个4 class Solution:def inte…

学习疲劳(科普)

由于长时间进行学习&#xff0c;在生理和心理方面产生了怠倦&#xff0c;致使学习效率下降&#xff0c;甚至到了不能继续学习的状况。疲劳现象有各种形态&#xff0c;主要可分为生理的&#xff08;或身体的&#xff09;疲劳和心理的疲劳。学习既包括身体的活动,也包括精神的活动…

LeetCode657.机器人能否返回原点

题目链接&#xff1a; 657. 机器人能否返回原点 - 力扣&#xff08;LeetCode&#xff09; 分析&#xff1a;很简单的模拟问题&#xff0c;同样属于手撕代码最喜欢的部分。 算法思路&#xff1a;定义一个x&#xff0c;y表示每个时刻机器人所在的位置&#xff0c;初始为&#…

python中内置函数简要介绍

pyton3.11版本中常用的内置函数&#xff0c;不需要导入&#xff0c;可直接使用。这些函数大多数都是比较常用的&#xff0c;很多在之前的文章都有介绍过。 大家也可直接到官网查看学习 https://docs.python.org/zh-cn/3.11/library/functions.html。 内置函数 abs() min() …

【CV】opencv调用DIS/LK等计算光流,前一帧和当前帧写反了有什么影响?

当在计算光流时&#xff0c;将前一帧和当前帧输入反了&#xff0c;会导致一系列问题。 在计算光流时&#xff0c;通常是将前一帧作为模板&#xff0c;根据当前帧计算光流。因为光流是描述相邻帧之间像素移动的一种方法&#xff0c;它通过比较两帧之间的像素强度或特征点的移动…

高质量新闻数据集OpenNewsArchive:880万篇主流新闻报道,国产大模型开源数据又添猛料

在构建国产大语言模型的道路上&#xff0c;高质量新闻是不可或缺的重要语料之一。这类语料集准确性、逻辑性、时效性于一体&#xff0c;同时包含丰富的事实知识&#xff0c;可以大幅提升模型的文本生成质量、词汇表达能力、事件理解分析能力以及时序内容的适应性和预测能力&…

《Python编程从入门到实践》day29

# 昨日知识点回顾 修改折线图文字和线条粗细 矫正图形 使用内置格式 # 今日知识点学习 15.2.4 使用scatter()绘制散点图并设置样式 import matplotlib.pyplot as plt import matplotlib matplotlib.use(TkAgg)plt.style.use(seaborn-v0_8) # 使用内置格式 fig, ax plt.subpl…

Linux写个脚本执行一系列命令

问题 很多时候需要执行一些列重复的命令&#xff0c;手动执行太麻烦&#xff0c;可以使用写个脚本依次执行。 解决 在Linux中&#xff0c;你可以编写一个Bash脚本文件&#xff0c;其中包含一系列命令&#xff0c;然后一次执行这些命令。 1.使用vim创建你的脚本 vim myscri…

LeetCode494:目标和

题目描述 给你一个非负整数数组 nums 和一个整数 target 。 向数组中的每个整数前添加 ‘’ 或 ‘-’ &#xff0c;然后串联起所有整数&#xff0c;可以构造一个 表达式 &#xff1a; 例如&#xff0c;nums [2, 1] &#xff0c;可以在 2 之前添加 ‘’ &#xff0c;在 1 之…