当前位置: 首页 > news >正文

list模拟实现(简单版)【C++】

目录

前言

1. list的私有成员

2. 构造函数

2.1 list构造函数

3. list遍历

3.1 push_back

3.2 ListIterator模拟实现

3.2.1 成员变量_node

3.2.2 it++ 和 ++it 

3.2.3 it-- 和 --it

3.2.4 *it

3.2.5 operator== 和 operator!=

3.2.6 operator->

3.3 begin 和 end

3.4 遍历测试

4. list 的增加和删除

4.1 empty 和 size

4.2 insert 和 erase

4.2.1 insert

4.2.2 erase

4.3 push_back(plus)、push_front 和 pop_back、pop_front

4.4 测试代码

5. const 类型迭代器

6. list析构函数 和 拷贝构造

6.1 析构函数

6.2 拷贝构造

6.3 operator=



前言

Q: 什么是list?

A: 参考标准库里面的解释std::list 

list就是一个序列容器,支持常数级别的时间复杂度的插入和删除。list底层的数据结构是带头双向循环链表,这样每个数据元素就可以在内存中是非相邻的。

如果对带头双向循环链表不熟悉的话,请猛戳这里带头双向循环链表

接下来 list的模拟实现的简单版。

文件准备:

在 vs 2022中创建头文件和测试文件

然后在list.h文件中创建my_list 命名空间,在my_list来模拟实现list类,在test.cpp文件中创建主函数来调用测试接口。

因为list底层的数据结构是带头结点的双向循环链表,这里需要一个节点的数据结构。

#pragma once
#include<iostream>
#include<assert.h>using namespace std;namespace my_list
{//双向链表结构体节点模板template<typename T>struct ListNode {ListNode<T>* _next;ListNode<T>* _prev;T _data;//节点的构造函数 用来初始化节点数据ListNode(const T& x = T()):_next(nullptr),_prev(nullptr),_data(x){}};//类似双向链表类模板template<class T>class list {typedef ListNode<T> Node;public:private:Node* _head;};void test_list1(){}
}

为什么使用 struct ?

因为里面的数据需要公开的,因为list本质是带头双向循环链表,不公开list就没法使用。

值得注意的是,节点的构造函数中的 T() 表示类型 T 的默认构造值。

当调用不提供参数时,默认使用类型T的默认值。

主要分为内置类型和自定义类型

  • 内置类型

    • int() → 0

    • double() → 0.0

    • bool() → false

    • char() → '\0'

    • 指针类型 → nullptr

  • 自定义类型

    • 调用该类型的默认构造函数

    • 如果没有默认构造函数,编译会报错

1. list的私有成员

因为list本质是带头双向循环链表,所以需要一个节点指针指向头节点,还需要一个计数器_size来统计节点个数。

	//list 类中的私有成员private:Node* _head;	//指向头结点size_t _size;	//记录结点个数

2. 构造函数

2.1 list构造函数

在不考虑内存池的情况下的list构造函数,首先创建一个节点,然后头节点的_next指向自己,最后头结点的_prev也指向自己。

list() 
{_head = new Node;               // 1. 创建一个头节点(哨兵节点)_head->_next = _head;           // 2. 头节点的 next 指向自己_head->_prev = _head;           // 3. 头节点的 prev 也指向自己_size = 0;                      // 4. 方便计算节点个数
}

3. list遍历

3.1 push_back

在实现遍历之前,首先保证list里面有元素,所以先实现一个尾插元素。这里与链表的尾插相似。

		//插入一个数据//尾插数据void push_back(const T& x){//传统写法//先申请一个节点Node* newnode = new Node(x);Node* tail = _head->_prev;	//指向最后一个元素节点tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;_size++;}

使用带头节点的优势就是:当只有一个头节点(哨兵卫)的情况 和 其他情况 进行插入节点都是一样的。

3.2 ListIterator模拟实现

list的遍历需要使用迭代器去遍历。使用迭代器的意义就是不管底层是什么,都可以进行访问。

Q:这里的原生指针可以充当迭代器吗?

A:不可以

因为list和顺序表不一样,在顺序表中,原生指针是天然的迭代器(前提是T*指向的物理空间是连续的);

而list中的原生指针Node*指向的物理空间是不连续的(因为节点是new出来的,不能保证每一个节点的地址都是连续的)。

既然list的原生指针不可以的话,所以封装一个类用自定义类型去重载运算符因为C++中的类和运算符重载可以去控制其行为。

迭代器用什么构造? 节点的指针就可以的,只不过是用类进行封装。

所以这要再命名空间my_list中额外写一个类进行控制。

3.2.1 成员变量_node

这里的使用_node来指向链表中的节点,这里要写成公有类,方便外部使用迭代器去调用。

	template<class T>struct ListIterator {//自定义类型封装指针,去控制其行为typedef ListNode<T> Node;	//模板类重命名为Nodetypedef ListIterator<T> Self;	//模板类重命名为SelfNode* _node;	// 指向当前迭代器所代表的链表节点的指针//构造函数ListIterator(Node* node) :_node(node)};

3.2.2 it++ 和 ++it 

因为原生指针不可以充当迭代器,所以这里使用专门封装的类中的运算符重载来进行控制。

3.2.2.1 it++

it++, 这里需要考虑的是先使用后++,返回的是之前的节点,先保存原来的节点,再进行++。

		//后置++,使用(int)来区分前置和后置Self operator++(int) {//后置++ 返回之前的值Self tmp(*this);	//这里调用了拷贝构造,指针内置类型的浅拷贝,因为希望指向同一个空间//迭代器也不需要写析构,因为节点不属于迭代器,节点属于链表,所以这里不需要析构_node = _node->_next;return tmp;}

注意:

Q :为什么后置++ 返回T 而不是 T& (或者 为什么后置++是返回临时变量) ?

A :这里返回临时变量,因为后置++ 返回的是自增前的旧值,而旧值是一个临时对象(不能返回局部变量的引用)。前置++ 返回的是自增后的对象本身

3.2.2.2 ++it

++it, 前置++,返回++以后的节点,_node 的下一个节点。

		//重载前置++Self& operator++() {_node = _node->_next;//前置++,返回++以后的值return *this;}

注意:

Q :前置++为什么返回引用?

A : 因为前置++ 返回的是自增后的对象本身(*this),而 *this 的生命周期,不会立即销毁。

值得注意的是,这里使用 operator(int)++ 来进行区分 前置与后置。

3.2.3 it-- 和 --it

这里自减与上述自增类似。

		// it--Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}// --itSelf& operator--() {_node = _node->_prev;return *this;}

3.2.4 *it

这里需要注意的是,解引用来获取data,不能传值返回,传值返回的是data的拷贝,因为*it 有读和写的功能,传引用返回就可以进行读写data。

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

3.2.5 operator== 和 operator!=

这里只需比较节点的指针就可以,两个迭代器如果它们里面的指针是相同的,它们就是相等的,不相同就是不相等的。

		bool operator==(const Self& it){return _node == it._node;}//!=bool operator!=(const Self& it){return _node != it._node;}

3.2.6 operator->

这里为了提高可读性,数据访问时,由it.operator->()->_a1 直接变为 it->_a1。

	//在C++11中支持多参数构造的隐式类型转换struct A {int _a1;int _a2;A(int a1 = 1,int a2 = 1):_a1(a1),_a2(a2){}};void test_list3() {list<A> lt;A aa1(2, 2);A aa2 = {3,3};lt.push_back(aa1);lt.push_back(A(2,2));lt.push_back({3,3});	//C++11多参数的隐式类型转换list<A>::iterator it = lt.begin();cout << it->_a1 << endl;cout << it.operator->()->_a1 << endl;}

3.3 begin 和 end

使用begin ,因为想使用_head->next 去构造节点,因为_head是私有的,所以使用公有的begin,返回第一个元素的迭代器。

begin返回头结点的下一个节点即可。

普通写法

		iterator begin(){//1 普通版iterator it = _head->_next;return it;}

匿名对象写法

iterator begin() {//2 匿名对象版return iterator(_head->_next);}

单参数构造写法

		iterator begin() {//3 单参数构造函数支持隐式类型转换//这里的迭代器就是单参数构造函数return _head->_next;//构造函数//ListIterator(Node * node)//	:_node(node)//{}}

end返回头结点即可(因为list本质是带头节点的双向循环链表)。

		iterator end() {//其他写法同begin类似return _head;}

3.4 遍历测试

这里可以在命名空间my_list中进行测试。

	void test_list1(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);//list的遍历需要使用迭代器list<int>::iterator it = lt.begin();while (it != lt.end()) {cout << *it << " ";++it;}cout << endl;}

输出结果 :

4. list 的增加和删除

4.1 empty 和 size

在实现list的增加和删除这里需要先实现,判空和计算节点个数的接口。

		bool empty() const{return _size == 0;}size_t size() const{return _size;}

4.2 insert 和 erase

4.2.1 insert

这里要实现一个 在pos节点之前插入一个值为val的函数。

先用cur指向pos节点,再创建一个值为val的节点,再用prev指向cur的前一个节点。

		void insert(iterator pos , const T& val) {Node* cur = pos._node;		//cur指向pos位置的节点Node* newnode = new Node(val);Node* prev = cur->_prev;//在cur前面插入一个节点// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;}

4.2.2 erase

删除pos位置的节点,链表只需修改指针域即可。

		iterator erase(iterator pos) {//避免空assert(!empty());//删除pos位置的节点//prev cur nextNode* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;--_size;//pos 失效 是 释放了pos位置的空间//为了避免失效,要返回一下节点的迭代器return iterator(next);}

值得注意的是,erase返回类型是iterator,避免迭代器失效,要返回下一个节点的迭代器。

4.3 push_back(plus)、push_front 和 pop_back、pop_front

这里借助上方实现的insert 和 erase 来进行实现。

		//push_back 现代写法void push_back(const T& x){insert(end(),x);}//头插void push_front(const T& x) {insert(begin(),x);}//尾删void pop_back() {erase(--end());	//注意这里不能end()-1 ,因为这里是使用运算符重载来实现的}//头删void pop_front() {erase(begin());}

值得注意的是,pop_back去调用end()时,不能使用end-1 ,运算符重载只实现了operator--()。

4.4 测试代码

在test.cpp中进行调用,test_list2() 函数在my_list命名空间中进行实现。

	void test_list2() {list<int> lt;lt.push_back(2);lt.push_front(1);lt.push_back(3);lt.push_back(4);lt.push_back(5);//整体遍历list<int>::iterator it = lt.begin();while (it != lt.end()) {cout << *it << " ";++it;}cout << endl;//头删lt.erase(lt.begin());it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;//头删lt.pop_front();//尾删lt.pop_back();for (auto e : lt) {cout << e << " ";}cout << endl;cout << "元素个数:"<<lt.size() << endl;}

5. const 类型迭代器

我们知道:

		const int* ptr;	// const 在*的左边,修饰的是指针指向的数据不能被修改int* const ptr = nullptr;	//const 在*右边,修饰的是指针不能被修改

我们知道权限可以缩小,但是不能放大。这里要实现的是迭代器指向的内容不能被修改。

具体实现:

方式一: 单独实现一个ListConstIterator去封装里面的迭代器指向的内容不能被修改。

	template<class T>struct ListConstIterator{//自定义类型封装指针,去控制其行为typedef ListNode<T> Node;	//模板类重命名为Nodetypedef ListConstIterator<T> Self;	//模板类重名为SelfNode* _node;	// 指向当前迭代器所代表的链表节点ListConstIterator(Node* node):_node(node){}//* const T& operator*(){return _node->_data;}//it->const T* operator->(){return &_node->_data;}//通过运算符重载控制其行为//重载前置++Self& operator++(){_node = _node->_next;//前置++,返回++以后的值return *this;}//后置++,使用(int)来区分前置和后置Self operator++(int){//后置++ 返回之前的值Self tmp(*this);	_node = _node->_next;return tmp;}// it--Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}// --itSelf& operator--(){_node = _node->_prev;return *this;}bool operator==(const Self& it){return _node == it._node;}//!=bool operator!=(const Self& it){return _node != it._node;}};

方式二:

发现方式一的写法有一些代码冗余,因为里面具体只是一些函数的返回值类型与Iterator类不同,所以可以考虑使用模板参数来控制。

本质:写一个模板类,然后编译器实例化生成两个类。

	template<class T,class Ref,class Ptr>struct ListIterator {//自定义类型封装指针,去控制其行为typedef ListNode<T> Node;	//模板类重命名为Nodetypedef ListIterator<T,Ref,Ptr> Self;	//模板类重名为SelfNode* _node;	// 指向当前迭代器所代表的链表节点ListIterator(Node* node) :_node(node){}//* Ref operator*() {return _node->_data;}//it->Ptr operator->(){return &_node->_data;}//通过运算符重载控制其行为//重载前置++Self& operator++() {_node = _node->_next;//前置++,返回++以后的值return *this;}//后置++,使用(int)来区分前置和后置Self operator++(int) {//后置++ 返回之前的值Self tmp(*this);_node = _node->_next;return tmp;}// it--Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}// --itSelf& operator--() {_node = _node->_prev;return *this;}bool operator==(const Self& it){return _node == it._node;}//!=bool operator!=(const Self& it){return _node != it._node;}};

6. list析构函数 和 拷贝构造

6.1 析构函数

先实现一个clear,借助迭代器和erase删除所有数据(不含头节点)。

		void clear() {//借助迭代器和eraseiterator it = begin();while (it != end()) {it = erase(it);	//前提是erase处理了迭代器失效问题}}

再去调用clear实现析构。

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

6.2 拷贝构造

值得注意的是,list的拷贝构造需要手动去实现,因为:

当不写拷贝构造,list会使用默认的拷贝构造(浅拷贝--指向同一块空间),但是 对同一块空间进行析构两次是错误的,因为第一次析构后对象就已经不在了,第二次可能导致释放了不该释放的内存!

先初始化一个头节点,再复用push_back,把lt的节点拷贝进去。

		void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}//list 构造函数list(){empty_init();}//lt1(lt2)list(const list<T>& lt){//先初始化一个头节点//再复用push_back,把lt的节点拷贝进去empty_init();for (auto& e : lt) {push_back(e);}}

需要析构,一般需要自己写深拷贝。

6.3 operator=

		void swap(list<T>& lt) {//借助标准库函数中的swap来实现std::swap(_head,lt._head);std::swap(_size,lt._size);}//lt2 = lt1list<T>& operator=(list<T> lt) {swap(lt);return *this;}

http://www.dtcms.com/a/442243.html

相关文章:

  • 烟台网站建设哪家好呢维护一个网站需要多少钱
  • 什么网站做视频最赚钱wordpress无法使用ajax
  • 对接MCP服务之sse/streamable-http模式
  • springMVC中/*与/**匹配的区别
  • 如何快速学习一个网络协议?
  • 从安装到上手实战——Docker 基础命令全解析
  • 虚拟机怎么做网站昆明seo技术培训
  • 免费dede企业网站模板wordpress qa
  • autodl 安装modelscope OCR 模型 dots_ocr 笔记心得
  • Linux中文件目录结构介绍以及对目录的操作
  • 大庆建设工程交易中心网站唐山建设信息网站
  • 第8章:扩展边界:技术之外的视野(2)
  • Java面向对象练习:Person类继承与排序
  • Day04_刷题niuke20251005
  • 四个字网站 域名网站开发项目进度表
  • 理解C++20的革命特性——协程支持1
  • 【八股】操作系统
  • 2025年第13批中国深度合成算法备案分析报告
  • 建设工程专业承包交易中心网站百度指数分析官网
  • SSMEE的仓库管理系统93c6b(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
  • 怎么自己学做网站建筑案例分析模板
  • 怎样申请一个免费网站全国加盟网站建设
  • 【均衡器调节原理与操作指南】
  • 个人静态网站首页怎么做扬州今天的最新发布消息
  • 北京软件开发公司排行榜最新网站seo优化主要有哪些手段
  • React18学习笔记(五) 【总结】常用的React Hooks函数,常用React-Redux Hooks函数和React中的组件通信
  • display this interface 概念及题目
  • 网站管理后台制作服务专业的网络建站公司
  • 研发管理 #项目管理 #APQP #IATF16949 #智能制造 #数字化转型
  • 网站做百度推广有没有效果悬浮网站底部代码