【C++】STL中List的基本功能的模拟实现

前言:在前面学习了STL中list的使用方法,现在我们就进一步的讲解List的一些基本功能的模拟实现,这一讲博主认为是最近比较难的一个地方,各位一起加油。

💖 博主CSDN主页:卫卫卫的个人主页 💞
👉 专栏分类:高质量C++学习 👈
💯代码仓库:卫卫周大胖的学习日记💫
💪关注博主和博主一起学习!一起努力!
在这里插入图片描述


目录标题

  • List的模拟实现
    • List三个基本类
      • 结点类接口的实现
      • list的正向迭代器类的实现
    • List正向迭代器的接口实现
      • 构造函数
      • operator*运算符重载
      • operator->运算符重载
      • operator前置++和--与后置++和--
      • operator==与operator!=
    • List类的接口的实现
      • 构造函数
      • begin()和end()
      • 尾插函数- push_back(const T& x)
      • insert(iterator pos, const T& val)插入(pos之前的位置)
      • push_front(const T& x)头插
      • iterator erase(iterator pos)删除pos位置的值
      • 尾删与头删
      • clear()清空list
      • size_t size() 查看链表元素
      • bool empty()查看链表元素是否为空
      • 拷贝构造函数
      • 析构函数
      • swap函数 交换链表中的元素
      • operator=运算符重载
    • 整体代码


List的模拟实现

List三个基本类

前面我们提到过,list本质上就是一个带头双向循环链表,这里我们要实现list的功能就要实现三个类:

  1. 模拟实现结点类
  2. 模拟实现迭代器的类
  3. 模拟list主要功能的类

结点类接口的实现

这里如果对带头双向链表不太熟悉的小伙伴可以去看看博主之前的文章带头双向循环链表

template <class T>
struct ListNode//链表的主体
{
	ListNode<T>* _prev;//C++中可不写struct,直接用名定义
	ListNode<T>* _next;
	T _data;//存节点的值
	ListNode(const T& x = T())//这个地方在讲模拟实现vector的时候也讲了,需要查看的可以看看之前的博客
		:_next(nullptr)
		, _prev(nullptr)
		, _data(x)
	{}
};

看到这里很多小伙伴会有疑问为什么这里写的是ListNode*

  1. 在C++中是可以省略struct不写的,也就是说原本的样子应该是 struct ListNode * _prev
  2. 结构体模板或类模板在定义时可以不加 T,但 使用时必须加T

list的正向迭代器类的实现

template<class T, class Ref, class Ptr>
struct ListIterator//迭代器
{
   typedef ListNode<T> Node;
   typedef ListIterator<T, Ref, Ptr> Self;//T表示基本类型, Ref表示引用返回,Ptr指代指针返回
   Node* _node;//记录链表
   ListIterator(Node* node)//传过来的位置就是迭代器从哪个位置开始
}

这里大部分人会有因为为什么这里迭代器的模板会有三个参数?因为如果我们只是使用普通迭代器的话确实一个参数就够了,但是有的情况我们是需要使用const迭代器的,难道我们还要在写一个类来专门放 const类型的迭代器嘛?
而后文list类的模拟实现中,我对迭代器进行了两种typedef:
普通迭代器:typedef ListIterator<T, T&, T*> iterator;
const迭代器:typedef ListIterator<T, const T&, const T*> const_iterator;
在这里插入图片描述


List正向迭代器的接口实现

构造函数

这里我们通过传过来的结点完成构造,让迭代器指向传过来结点的位置即可

ListIterator(Node* node)//传过来的位置就是迭代器从哪个位置开始
	:_node(node)
{
}

operator*运算符重载

前面我们说到过,Ref本质就是引用返回,无非就是const还是非const的类型的区分

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

operator->运算符重载

至于为什么要写->的运算符重载,就是我们在list的使用的过程中传过去的不一定就是内置类型,还有可能是自定义类型(如下所示)

struct A
{
	int _a1;
	int _a2;
	
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{}
};	
void test_list2()
{
	list<A> lt;
	A aa1(1, 1);
	A aa2 = { 1, 1 };
	lt.push_back(aa1);
	lt.push_back(aa2);
	lt.push_back(A(2, 2));
	lt.push_back({ 3, 3 });
	lt.push_back({ 4, 4 });


	list<A>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << it->_a1 << ":" << it->_a2 << endl;
		//本质上编译器会省略一个->,所以实际上写的是it->_A->_a1
		cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;

		++it;
	}
	cout << endl;
}
Ptr operator->()//本质上就是重载自定义类型,帮你找到内置类型,然后再找到内置类型的数据
{
	return &_node->_data;
}

operator前置++和–与后置++和–

这里我们提一下,对于前置++和后置++还有–等,我们主要传一个int类型的数据来进行区分

Self& operator++()//前置++
{
	_node = _node->_next;
	return *this;
}

Self operator++(int)//后置++,加上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;
}

operator==与operator!=

代码思路:对于如何判断两个迭代器是否相等,我们只需要判断两个迭代器所指向的位置是否相等即可。

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

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

List类的接口的实现

代码思路:这里我们主要是通过两个迭代器帮助我们去遍历list,然后一个const迭代器是只读的作用,一个非const迭代器是即可读又可写的作用

template <class T>
class list//链表
{
	typedef ListNode<T> Node;
public:
	typedef ListIterator<T, T&, T*> iterator;//正向迭代器
	typedef ListIterator<T, const T&, const T*> const_iterator;//const迭代器
private:
	Node* _head;
	size_t _size;//记录链表元素个数
};

构造函数

这里我们就采用双向带头链表的思路,初始化的时候让其的前驱指针和next指向他的哨兵位即可。

void empty_init()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
}
list()//默认构造
{
	empty_init();
}

begin()和end()

iterator begin()//begin应该是哨兵位的下一个结点
{
	return _head->_next;
}
iterator end()//因为是带头双向链表,所以通常没有尾部的这个说法,一般结束的时候就是在哨兵位这个结点就是尾结点
{
	return _head;
}

const_iterator begin()const//只读的版本
{
	return _head->_next;
}

const_iterator end() const
{
	return _head;
}

尾插函数- push_back(const T& x)

关于尾插这部分的内容,我们在之前数据结构那部分讲的挺详细的不懂的话可以看看博主之前的博客。

void push_back(const T& x)//尾插
{
	//insert(end(), x);
	Node* tail = _head->_prev;//找尾
	Node* newnode = new Node(x);//创建一个新的结点
	tail->_next = newnode;
	newnode->_prev = tail;
	//使newnode和头结点_head构成循环
	newnode->_next = _head;
}

insert(iterator pos, const T& val)插入(pos之前的位置)

这里我们会发现使用insert会改变了底层,会导致迭代器失效,所以使用的时候要及时更新迭代器。

void insert(iterator pos, const T& val)//插入
{
	Node* cur = pos._node;//找到当前结点的链表
	Node* newnode = new Node(val);
	Node* prev = cur->_prev;
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
	_size++;
}

push_front(const T& x)头插

这里我们可以顺带把尾插也给优化一下

void push_front(const T& x)//头插
{
	insert(begin(), x);
}
void push_back(const T& x)//尾插
{
	insert(end(), x);
}

iterator erase(iterator pos)删除pos位置的值

这里我们也需要注意的是,删除和插入数据都会导致迭代器失效,因此我们需要及时的更新迭代器

iterator erase(iterator pos)//删除会导致迭代器失效,故因此要返回迭代器的下一个位置
{
	assert(pos != end());
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;
	prev->_next = next;
	next->_prev = prev;
	delete cur;
	_size--;
	return iterator(next);
}

尾删与头删

void pop_back()//尾删
{
	erase(end() - 1);
}

void pop_front()
{
	erase(begin());
}

clear()清空list

void clear()
{
	iterator it = begin();//通过迭代器依次遍历清除
	while (it != end())
	{
		it = erase(it);
	}
}

size_t size() 查看链表元素

size_t size() const
{
	return _size;
}

bool empty()查看链表元素是否为空

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

拷贝构造函数

代码思路:我们只需要对链表的元素依次尾插到新的链表中即可

list(const list<T>& lt)
{
	empty_init();
	for (auto& e : lt)
	{
		push_back(e);
	}
}

析构函数

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

swap函数 交换链表中的元素

void swap(list<T>& it)//it要被修改
{
	std::swap(_head, it._head);
	std::swap(_size, it._size);
}

operator=运算符重载

list<T>& operator=(list<T> it)
{
	swap(*this,it);
	return *this;
}

整体代码

#include<iostream>
#include <assert.h>
using namespace std;

namespace bit
{
	template <class T>
	struct ListNode//链表的主体
	{
		ListNode* _prev;//C++中可不写struct,直接用名定义
		ListNode* _next;
		T _data;
		ListNode(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct ListIterator//迭代器
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;//T表示基本类型, Ref表示引用返回,Ptr指代指针返回
		Node* _node;//记录链表
		ListIterator(Node* node)//传过来的位置就是迭代器从哪个位置开始
			:_node(node)
		{
		}

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

		// list<int>::ListIterator it;  it->data;
		//list<Data>::ListIterator it;  it->Data->data;
		Ptr operator->()//本质上就是重载自定义类型,帮你找到内置类型,然后再找到内置类型的数据
		{
			return &_node->_data;
		}

		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& it)
		{
			return _node != it._node;
		}

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


	template <class T>
	class list//链表
	{
		typedef ListNode<T> Node;
	public:
		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;
		}

		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}
		list()//默认构造
		{
			empty_init();
		}

		// lt2(lt1)
		list(const list<T>& lt)
		{
			empty_init();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		~list()//析构
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		void swap(list<T>& it)//it要被修改
		{
			std::swap(_head, it._head);
			std::swap(_size, it._size);
		}

		list<T>& operator=(list<T> it)
		{
			swap(*this,it);
			return *this;
		}

		void push_back(const T& x)//尾插
		{
			//insert(end(), x);
			Node* tail = _head->_prev;//找尾
			Node* newnode = new Node(x);//创建一个新的结点
			tail->_next = newnode;
			newnode->_prev = tail;
			//使newnode和头结点_head构成循环
			newnode->_next = _head;
		}

		void push_front(const T& x)//头插
		{
			insert(begin(), x);
		}

		void insert(iterator pos, const T& val)//插入
		{
			Node* cur = pos._node;//找到当前结点的链表
			Node* newnode = new Node(val);
			Node* prev = cur->_prev;
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			_size++;
		}

		iterator erase(iterator pos)//删除会导致迭代器失效,故因此要返回迭代器的下一个位置
		{
			assert(pos != end());
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			prev->_next = next;
			next->_prev = prev;
			delete cur;
			_size--;
			return iterator(next);
		}
		
		void pop_back()//尾删
		{
			erase(end() - 1);
		}

		void pop_front()
		{
			erase(begin());
		}

		size_t size() const
		{
			return _size;
		}

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

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}
	private:
		Node* _head;
		size_t _size;
	};

好啦,今天的内容就到这里啦,下期内容预告stl中stack和queue的使用与模拟实现.


结语:今天的内容就到这里吧,谢谢各位的观看,如果有讲的不好的地方也请各位多多指出,作者每一条评论都会读的,谢谢各位。


🌏🗺️ 这里祝各位接下来的每一天好运连连 💞💞

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unity DOTS技术(九) BufferElement动态缓冲区组件

文章目录 一.简介二.例子 一.简介 在之前的学习中我们发现Entity不能挂载相同的组件的. 当我们需要用相同的组件时则可以使用.IBufferElementData接口 动态缓冲区组件来实现 二.例子 1.创建IBufferElementData组件 using Unity.Entities; using UnityEngine; //[GenerateAu…

C语言基础——函数

ʕ • ᴥ • ʔ づ♡ど &#x1f389; 欢迎点赞支持&#x1f389; 个人主页&#xff1a;励志不掉头发的内向程序员&#xff1b; 专栏主页&#xff1a;C语言基础&#xff1b; 文章目录 前言 一、函数的概念 二、库函数 2.1 库函数和头文件 2.2 库函数的使用/…

10.【机器学习】十大算法之一决策树(Decision tree)算法原理讲解

【机器学习】十大算法之一决策树&#xff08;Decision tree&#xff09;算法原理讲解 一摘要二个人简介三什么是决策树四什么是树4.1 二叉树4.1.1 特殊的二叉树&#xff1a;4.1.2 举例说明 五决策树的优缺点5.1 优点5.2 缺点 六算法原理七信息熵八信息增益九信息增益率十基尼指…

2 - 寻找用户推荐人(高频 SQL 50 题基础版)

2.寻找用户推荐人 考点: sql里面的不等于&#xff0c;不包含null -- null 用数字判断筛选不出来 select name from Customer where referee_id !2 OR referee_id IS NULL;

uniapp中使用百度ocr识别引入项目

uniapp中使用百度ocr识别引入项目 官网申请地址 orcAPI文档地址 1.先获取token const getToken () > {uni.request({url: https://aip.baidubce.com/oauth/2.0/token,method: POST,data: {grant_type: client_credentials,client_id: **, apikeyclient_secret: **, skey…

实现开源可商用的 ChatPDF RAG:密集向量检索(R)+上下文学习(AG)

实现 ChatPDF & RAG&#xff1a;密集向量检索&#xff08;R&#xff09;上下文学习&#xff08;AG&#xff09; RAG 是啥&#xff1f;实现 ChatPDF怎么优化 RAG&#xff1f; RAG 是啥&#xff1f; RAG 是检索增强生成的缩写&#xff0c;是一种结合了信息检索技术与语言生成…

刷代码随想录有感(94):划分字母区间(怪!)

题干&#xff1a; 代码&#xff1a; class Solution { public:vector<int> partitionLabels(string s) {int hash[26] {0};for(int i 0; i < s.size(); i) hash[s[i] - a] i;vector<int> res;int left 0;int right 0;for(int i 0; i < s.size(); i){r…

算法2:滑动窗口(下)

文章目录 水果成篮找到字符串中所有字母异位词串联所有单词的子串*最小覆盖子串* 水果成篮 两元素排空操作 窗口中存在元素交错情况&#xff0c;所以出窗口一定要出干净&#xff01;&#xff01;&#xff01; class Solution { public:int totalFruit(vector<int>& …

AI图书推荐:《如何利用ChatGPT在线赚钱》

这本书《如何利用ChatGPT在线赚钱》&#xff08;$100m ChatGPT_ How To Make Money Online With ChatGPT -- Sharp, Biily -- 2023 &#xff09;主要阐述如何利用ChatGPT这一强大的语言模型工具在互联网上创造收入。 以下是各章节内容的概要&#xff1a; **引言** - 介绍了Chat…

机器学习——多层感知机

感知机 缺点&#xff1a;只能处理线性问题&#xff0c;感知机无法解决异或问题 在这里偏置就像线性模型的常数项&#xff0c;加入偏置模型的表达能力增强&#xff0c;而激活函数就像示性函数&#xff0c;可以模拟神经元的兴奋和抑制&#xff0c;当大于等于0就输出1。 多层感…

从军事角度理解“战略与战术”

战略与战术&#xff0c;均源于军事术语。 战略&#xff08;Strategy&#xff09;&#xff0c;源自希腊语词汇“strategos&#xff08;将军&#xff09;”和“strategia&#xff08;军事指挥部&#xff0c;即将军的办公室和技能&#xff09;”。指的是指挥全局性作战规划的谋略…

网络基础_02

1.ARP协议 地址解析协议&#xff08;Address Resolution Protocol&#xff09; 已知对方的三层ip地址&#xff0c;需要二层mac地址 当一台设备&#xff08;请求方&#xff09;需要知道某个 IP 地址对应的 MAC 地址时&#xff0c;会使用 ARP封装一个数据帧。这台设备的网络层以…

[职场] 为什么不能加薪? #学习方法#知识分享#微信

为什么不能加薪&#xff1f; 不能加薪的根本原因&#xff0c;终于被我找到了&#xff01; 朋友们&#xff01;职场这个地方是个很神奇的世界&#xff0c;有些规则并不是你想象的那样。我们都希望能在这个世界里施展自己的才华&#xff0c;获得升职加薪的荣耀。然而&#xff0c…

Blender 学习笔记(二)游标与原点

1. 游标 游标是界面中的红色圆圈&#xff1a; 1.1 移动游标 我们可以通过点击工具栏中的游标按钮&#xff0c;来移动游标&#xff0c;或者通过快捷键 shift右键 移动。若想要重置复游标位置&#xff0c;可以用 shiftc 恢复&#xff0c;或则通过 shifts 点击 游标->世界原…

Python中的Paramiko与FTP文件夹及文件检测技巧

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; Python代码的魅力与实用价值 在当今数字化时代&#xff0c;编程已成为一种不可或缺的技能。Python作为一种简洁、易读且功能强大的编程语言&#xff0c;受到了全球开发者的喜爱。它不仅适用于初学者入门&#xff0c…

Java 数据库连接(JDBC)的使用,包括连接数据库、执行SQL语句等

一、简介 Java Database Connectivity&#xff08;JDBC&#xff09;是Java应用程序与关系数据库进行交互的一种API。它提供了一组用于访问和操作数据库的标准接口&#xff0c;使开发人员能够使用Java代码执行数据库操作&#xff0c;如查询、插入、更新和删除等。 二、JDBC架构…

C++ AVL树 详细讲解

目录 一、AVL树的概念 二、AVL树的实现 1.AVL树节点的定义 2.AVL树的插入 3.AVL树的旋转 4.AVL树的验证 三、AVL树的性能 四、完结撒❀ 一、AVL树的概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但 如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查 …

SQL进阶day11——窗口函数

目录 1专用窗口函数 1.1 每类试卷得分前3名 1.2第二快/慢用时之差大于试卷时长一半的试卷 1.3连续两次作答试卷的最大时间窗 1.4近三个月未完成试卷数为0的用户完成情况 1.5未完成率较高的50%用户近三个月答卷情况 2聚合窗口函数 2.1 对试卷得分做min-max归一化 2.2每份…

m3u8视频怎么打开?教你三招!

m3u8 是一种文本文件格式&#xff0c;用于创建媒体播放列表&#xff0c;现在大部分的视频流媒体都是m3u8格式。当我们从网上下载下来m3u8文件的时候会发现&#xff0c;它本身不是一段视频&#xff0c;而是一个索引纯文本文件。想要正常打开播放m3u8视频其实也很简单&#xff0c…

解码 LangChain | LangChain + GPTCache =兼具低成本与高性能的 LLM

LangChain 联合创始人 Harrison Chase 提到&#xff0c;多跳问题会给语义检索带来挑战&#xff0c;并提出可以试用 AI 代理工具解决。不过&#xff0c;频繁调用 LLM 会导致出现使用成本高昂的问题。 对此&#xff0c;Zilliz 软件工程师 Filip Haltmayer 指出&#xff0c;将 GP…