【C++】list的使用 | 模拟实现
迪丽瓦拉
2025-05-29 15:11:43
0

文章目录

  • 一、list的使用
    • 1. 构造函数
    • 2. 迭代器
    • 3. 增删查改
    • 4. 其他成员函数
  • 二、list的模拟实现
    • 1. 节点的创建
    • 2. push_back和push_front
    • 3. 普通迭代器
    • 4. const迭代器
    • 5. 增删查改(insert、erase、pop_back、pop_front)
    • 7. 构造和析构
  • 三、list模拟实现整体源码
  • 四、vector和list的区别


一、list的使用

前面我们在数据结构阶段讲过:【双向带头循环链表】,其实我们前面所讲过的双向带头循环链表就是基于STL中的list所实现的。双向带头循环链表的整体结构如下:
在这里插入图片描述

对于list,我们需要注意以下几点:

  1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
  2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
  3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
  4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好
  5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)

1. 构造函数

在这里插入图片描述

int main()
{//默认构造list lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);//拷贝构造list lt2(lt);//迭代器区间构造list lt3(lt.begin(), lt.end());//构造n个vallist lt4(5, 1);return 0;
}

2. 迭代器

在这里插入图片描述

此处,大家可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。

在这里插入图片描述

注意:

  1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
  2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动。

3. 增删查改

在这里插入图片描述

这里我们需要注意的是:由于list是一个链表,它的物理地址是不连续的,所以list不支持[]访问,list的空间是使用一个开辟一个,所以他也是不支持reserve操作的。


4. 其他成员函数

在这里插入图片描述

sort——链表排序

我们知道算法库中是有一个sort函数的,但由于list中节点的空间是不连续的,所以不能使用算法库中的sort函数,所以list自己提供了一个sort函数,但是这个接口我们一般也不会使用,因为链表排序的效率实在是太低了。

unique——链表去重

我们在使用链表去重之前需要保证链表有序,否则就会导致去重不完全。

remove——移除链表中的指定元素

remove用来移除链表中的指定元素,如果元素不存在,则此接口什么都不会做。

merge——归并两个有序链表

这里我们需要注意的是,两个链表必须有序才能够调用这个函数来进行归并,而且归并之后,被归并的一方的元素将会消失,相当于把一方中的元素转移到了另一方中,而且,归并后的链表依然保持有序。


二、list的模拟实现

1. 节点的创建

//节点的封装
template
struct list_node
{list_node* _prev;list_node* _next;T _val;//构造函数list_node(const T& val = T()):_prev(nullptr),_next(nullptr),_val(val){}
};

首先,由于节点存储的数据可能是任意类型,所以我们需要将将节点定义为模板类。这里我们需要写一个给缺省值的默认构造函数,便于之后在主类中new一个新节点时直接初始化,同时将两个指针置为空,将数据写入数据域中。这里还有一点需要注意的是:在类内,不管是否存在模板,类名都可以作为类型,但是为了避免混淆我们不建议这样使用。


2. push_back和push_front

由于我们需要在list类中的成员变量需要定义一个list_node*类型的指针,而且我们在后面会经常用到list_node这种类型。所以在这里我们直接将这个类型typedef一下。

💕 list类的整体框架:

class list{
public:typedef list_node node;//成员方法...
private:node* _head;
}
//尾插
void push_back(const T& x) const
{node* new_node = new node(x);node* tail = _head->_prev;//链接节点之间的关系tail->_next = new_node;new_node->_prev = tail;new_node->_next = _head;_head->_prev = new_node;
}
//头插
void push_front(const T& x)
{node* head = _head->_next;node* new_node = new node(x);_head->_next = new_node;new_node->_prev = _head;new_node->_next = head;head->_prev = new_node;
}

这里的头插和尾插也很简单,因为和我们之前在数据结构时候的双向循环链表是一样的,只需要找到头或者尾,然后链接四个节点间的关系即可。


3. 普通迭代器

因为迭代器是类似于指针的存在,迭代器要实现指针相关的全部或者部分操作,+、-、++、- -、*、我们之前的string和vector使用的就是原生指针,所以他天然就支持上述的所有操作,但是list就不一样了。

因为list的节点是一个结构体,同时list的每一个节点的物理地址是不连续的,所以如果我们直接使用普通的原生指针来作为list的迭代器的话就不能够进行,解引用、++、–等操作,所以我们需要将迭代器封装为一个类,在类中实现这些操作的运算符重载才能够得到最终的结果。

template
struct __list_iterator
{typedef list_node node;typedef __list_iterator self;//构造函数__list_iterator(node* n):_node(n){}node* _node;//重载*运算符T& operator*(){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& s){return _node != s._node;}//重载==运算符bool operator==(const self& s){return _node == s._node;}
};

我们实现一个简单的正向迭代器是很容易的,使用一个模板参数T表示类型,当然这里我们可以将__list_iterator来typedef一下,方便后续使用。最后将这些运算符都重载一遍即可。

普通迭代器封装好了之后,我们需要在list类中来实现它的begin()和end()方法。由于迭代器的名字一般都是iterator,而且对于范围for来说,也只能通过iterator来转换为迭代器进行遍历。所以这里我们将其typedef为iterator。

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

4. const迭代器

const迭代器与普通迭代器的区别在于const迭代器指向的内容是不能修改的,但是它的指向是可以修改的,但在这里有些人可能会投机取巧用以下的方式来实现const迭代器:

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

这里我们千万要注意这种方式是不行的。因为const修饰的是iterator的返回值,即iterator的指向是不能修改的,如果我们这样写的话那么迭代器就不能++和–操作了,而解引用后的内容依旧是可以修改的。

在这里我们最好的做法就是在__list_iterator的类模板中的添加一个模板参数,然后再list类中typedef两份分别将第二个参数分别改成T&const T&的类型,本质上就是让编译器根据传入的Ref的不同来自动示例化出const迭代器类,具体的解决做法如下:

template
struct __list_iterator
{typedef list_node node;typedef __list_iterator self;//构造函数__list_iterator(node* n):_node(n){}node* _node;//重载*运算符Ref operator*(){return _node->_val;}//...
};
//list类中typedef两份
typedef __list_iterator iterator;
typedef __list_iterator const_iterator;

这里还有一个问题就是:如果我们需要重载一个->运算符,因为list中可能存储的是自定义类型,这个自定义类型如果要是有多个成员变量的话,我们就需要使用->来解引用访问成员变量。这里又出现问题了,因为又有普通迭代器和const迭代器的区分,所以为了解决同样的问题,我们同样选择多添加一个模板参数来解决这个问题。

template
struct __list_iterator
{typedef list_node node;typedef __list_iterator self;//构造函数__list_iterator(node* n):_node(n){}node* _node;//重载*运算符Ref operator*(){return _node->_val;}//重载->Ptr operator->(){return &_node->_val;}//...
};
typedef __list_iterator iterator;
typedef __list_iterator const_iterator;

下面我们来总结一下完整版的list迭代器的实现:

template
struct __list_iterator
{typedef list_node node;typedef __list_iterator self;//构造函数__list_iterator(node* n):_node(n){}node* _node;//重载*运算符Ref operator*(){return _node->_val;}//重载->Ptr operator->(){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& s){return _node != s._node;}//重载==运算符bool operator==(const self& s){return _node == s._node;}
};
//list类内
typedef __list_iterator iterator;
typedef __list_iterator const_iterator;iterator begin()
{return iterator(_head->_next);
}const_iterator begin() const
{return const_iterator(_head->_next);
}iterator end()
{return iterator(_head);
}const_iterator end() const
{return const_iterator(_head);
}

5. 增删查改(insert、erase、pop_back、pop_front)

//指定位置插入
void insert(iterator pos,const T& x)
{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;
}
//指定位置删除
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);
}//尾删
void pop_back()
{erase(--end());
}//头删
void pop_front()
{erase(begin());
}

7. 构造和析构

默认构造

由于我们将来在构造新的对象的时候会经常用到创建一个头结点的操作,所以这里我们在写默认无参构造的时候直接将它写成一个函数,以便于后面的调用。

list()
{empty_Init();
}
void empty_Init()
{_head = new node;_head->_next = _head;_head->_prev = _head;
}

拷贝构造

//拷贝构造传统写法
list(const list& lt)
{empty_Init();for (auto e : lt){push_back(e);}
}
//拷贝构造现代写法
list(const list& lt)
{empty_Init();list tmp(lt.begin(), lt.end());swap(tmp);
}

迭代器区间构造

template 
list(Iterator first, Iterator last)
{empty_Init();while (first != last){push_back(*first);first++;}
}
//交换函数
void swap(list& lt)
{std::swap(_head, lt._head);
}

用n个val来构造对象

//用n个val构造对象
list(int n, const T& val = T())
{empty_Init();for (int i = 0; i < n; i++){push_back(val);}
}

赋值运算符重载

//赋值运算符重载——传统写法
list& operator=(const list& lt)
{if (this != <){clear();for (const auto& e : lt){push_back(e);}}return *this;
}
//赋值运算符重载
list& operator=(list lt)//注意这里不能用引用
{swap(lt);return *this;
}

析构函数

//clear函数的实现
void clear()
{iterator it = begin();while (it != end()){it = erase(it);//erase(it++);  这种写法也是可以的}
}
//析构函数
~list()
{clear();delete _head;_head = nullptr;
}

size 和 empty

//size()
size_t size() const
{iterator it = begin();size_t Size = 0;while (it != end()){++Size;++it;}return Size;
}//empty
bool empty() const
{return _head->_next == _head&& _head->_prev == _head;
}

三、list模拟实现整体源码

namespace cjl
{//节点的封装templatestruct list_node{list_node* _prev;list_node* _next;T _val;//构造函数list_node(const T& val = T()):_prev(nullptr),_next(nullptr),_val(val){}};//迭代器的封装——普通迭代器//迭代器可能是原生指针,也可能是自定义类型对原生指针的封装,模拟指针的行为templatestruct __list_iterator{typedef list_node node;typedef __list_iterator self;//构造函数__list_iterator(node* n):_node(n){}node* _node;//重载*运算符Ref operator*(){return _node->_val;}//重载->Ptr operator->(){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& s){return _node != s._node;}//重载==运算符bool operator==(const self& s){return _node == s._node;}};//迭代器的封装——const迭代器——这样写造成了严重的代码冗余//template//struct __list_const_iterator//{//	typedef list_node node;//	typedef __list_const_iterator self;//	//构造函数//	__list_const_iterator(node* n)//		:_node(n)//	{}//	node* _node;//	//重载*运算符//	const T& operator*()//	{//		return _node->_val;//	}//	//重载前置++运算符//	self& operator++()//	{//		_node = _node->_next;//		return *this;//	}//	//重载!=运算符//	bool operator!=(const self& s)//	{//		return _node != s._node;//	}//};templateclass list{public:typedef list_node node;typedef __list_iterator iterator;//typedef __list_const_iterator const_iterator;typedef __list_iterator const_iterator;iterator begin(){return iterator(_head->_next);}const_iterator begin() const{return const_iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator end() const{return const_iterator(_head);}//默认的无参构造list(){empty_Init();}void empty_Init(){_head = new node;_head->_next = _head;_head->_prev = _head;}//用n个val构造对象list(int n, const T& val = T()){empty_Init();for (int i = 0; i < n; i++){push_back(val);}}//析构函数~list(){clear();delete _head;_head = nullptr;}//拷贝构造/*list(const list& lt){empty_Init();for (auto e : lt){push_back(e);}}*///拷贝构造——现代写法list(const list& lt){empty_Init();list tmp(lt.begin(), lt.end());swap(tmp);}//迭代器区间构造template list(Iterator first, Iterator last){empty_Init();while (first != last){push_back(*first);first++;}}赋值运算符重载——传统写法//list& operator=(const list& lt)//{//	if (this != <)//	{//		clear();//		for (const auto& e : lt)//		{//			push_back(e);//		}//	}//	return *this;//}//赋值运算符重载list& operator=(list lt)//注意这里不能用引用{swap(lt);return *this;}//交换函数void swap(list& lt){std::swap(_head, lt._head);}//尾插void push_back(const T& x) const{node* new_node = new node(x);node* tail = _head->_prev;//链接节点之间的关系tail->_next = new_node;new_node->_prev = tail;new_node->_next = _head;_head->_prev = new_node;}//尾删void pop_back(){erase(--end());}//头插void push_front(const T& x){node* head = _head->_next;node* new_node = new node(x);_head->_next = new_node;new_node->_prev = _head;new_node->_next = head;head->_prev = new_node;//insert(begin(), x);}//头删void pop_front(){erase(begin());}//指定位置插入void insert(iterator pos,const T& x){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;}//指定位置删除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);}//clear函数的实现void clear(){iterator it = begin();while (it != end()){it = erase(it);//erase(it++);  这种写法也是可以的}}//size()size_t size() const{iterator it = begin();size_t Size = 0;while (it != end()){++Size;++it;}return Size;}//emptybool empty() const{return _head->_next == _head&& _head->_prev == _head;}private:node* _head;};
}

四、vector和list的区别

在这里插入图片描述


相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
A.机器学习入门算法(三):基... 机器学习算法(三):K近邻(k-nearest neigh...
数字温湿度传感器DHT11模块... 模块实例https://blog.csdn.net/qq_38393591/article/deta...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
Redis 所有支持的数据结构... Redis 是一种开源的基于键值对存储的 NoSQL 数据库,支持多种数据结构。以下是...
win下pytorch安装—c... 安装目录一、cuda安装1.1、cuda版本选择1.2、下载安装二、cudnn安装三、pytorch...
MySQL基础-多表查询 文章目录MySQL基础-多表查询一、案例及引入1、基础概念2、笛卡尔积的理解二、多表查询的分类1、等...
keil调试专题篇 调试的前提是需要连接调试器比如STLINK。 然后点击菜单或者快捷图标均可进入调试模式。 如果前面...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
IHome主页 - 让你的浏览... 随着互联网的发展,人们越来越离不开浏览器了。每天上班、学习、娱乐,浏览器...
TCP 协议 一、TCP 协议概念 TCP即传输控制协议(Transmission Control ...
营业执照的经营范围有哪些 营业执照的经营范围有哪些 经营范围是指企业可以从事的生产经营与服务项目,是进行公司注册...
C++ 可变体(variant... 一、可变体(variant) 基础用法 Union的问题: 无法知道当前使用的类型是什...
血压计语音芯片,电子医疗设备声... 语音电子血压计是带有语音提示功能的电子血压计,测量前至测量结果全程语音播报࿰...
MySQL OCP888题解0... 文章目录1、原题1.1、英文原题1.2、答案2、题目解析2.1、题干解析2.2、选项解析3、知识点3...
【2023-Pytorch-检... (肆十二想说的一些话)Yolo这个系列我们已经更新了大概一年的时间,现在基本的流程也走走通了,包含数...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
记录--我在前端干工地(thr... 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间接触了Th...
43 openEuler搭建A... 文章目录43 openEuler搭建Apache服务器-配置文件说明和管理模块43.1 配置文件说明...