由浅到深-模拟实现list

news2024/11/18 19:37:46

前言

作者:小蜗牛向前冲

名言:我可以接受失败,但我不能接受放弃

如果觉的博主的文章还不错的话,还请点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。

目录

一 、见见STL中的list

1、list的介绍

 2、list的常见接口

二、list的模拟实现

1、list框架搭建

2、模拟实现list迭代器

3、list整体实现 

三、list和vector的对比

1、对比二者的优缺点

2、list和vector的排序效率 


 本期学习目标:认识STL中的list,模拟实现list,对list的迭代器深入理解,对比list和vector。

一 、见见STL中的list

1、list的介绍

下面我们了看看cpulcpul官网中的介绍:

 文档介绍:

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

从上面的介绍中我们初步认识到了list的是带头双向链表,对于要掌握的数据结构之一,下面我们一起来回忆一下他的增删查改操作。 

 2、list的常见接口

list的有很多接口,下面我们主要介绍几个重点接口:

list的构造

因为list在C++中是用类来封装的,他也就有自己的构造函数,但由于list初始化的场景非常多,所以他有多个构造函数,下面的在模拟实现的时候可以细细体会,下面我们先见见有哪些构造函数:

构造函数(Construct)

接口说明

list (size_type n, const value_type& val = value_type())

构造的list中包含n个值为val的元素

list()

构造空的list

list (const list& x)

拷贝构造函数

list (InputIterator first, InputIterator last)

用[first, last)区间中的元素构造list

list modifiers 

为来对list进行修改,也提供了一些修改的接口:

函数声明

接口说明

push_front

在list首元素前插入值为val的元素

pop_front

删除list中第一个元素

push_back

在list尾部插入值为val的元素

pop_back

删除list中最后一个元素

insert

在list position 位置中插入值为val的元素

erase

删除list position位置的元素

swap

交换两个list中的元素

clear

清空list中的有效元素

二、list的模拟实现

为了更好的理解list的底层实现,下面将大家一起去模拟实现list。

1、list框架搭建

我们要模式实现list,而list是个带头双向链表,那么我们首先搭建一个list_node的类模板

	struct list_node
	{
		list_node<T>*  _next;//指向后一个节点
		list_node<T>*  _prev;//指向前一个节点
		T _data;//节点中的数据
		list_node(const T& x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
	};

这里我们要注意的是我们不仅仅定义了节点的指向,我们还应该对节点进行初始化。

有了节点,那么我们就应该定义list类的主体,他的私有变量应该要有指向list_node的指针head,和记录链表个数的size,为了方便定义,这里我们可以直接对list_node的变量名重定义。

	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
        //各种成员函数
	private:
		node* _head;
		size_t _size;
	};

下面我们就要实现各种成员函数就可以了,但是在实现成员函数之前,我们要先实现list的迭代器。

2、模拟实现list迭代器

 我们在模式实现vector的迭代器的时候,认为迭代器就是一个指针。那么我们这里也可以把list的迭代器当作指针实现吗?这里显然是不可以的,为什么这么说呢?

当一个指针++他跳过的是他的一个类型的大小,但是list节点并不是挨个存储的他节点的空间是随机的,节点间是依靠节点中存放对方的地址指向对方的。

其实不仅仅++操作不满足,还有许多操作都是不满足的,如--操作。

我们又该如何解决这个问题呢?

其实我们可以用一个类模板,包含迭代器功能的成员函数,就可以解决。当我们调用迭代器时其实就是调用类模板中的成员函数。

但是这里要注意一个细节:由于成员函数他的返回值可能存在类型的差异,比如:*解引用的时候,返回_pnode->_data,但是->的时候是&_pode->_data;

这样类模板的参数就不仅仅是一个模板参数,而要三个模板参数才能解决。

//定义迭代器
	template <class T,class Ref,class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref, Ptr> Self;
		node* _pnode;
		//初始化
		__list_iterator(node* p)
			:_pnode(p)
		{}

		Ptr operator->()
		{
			return &_pnode->_data;
		}
		Ref operator*()
		{
			return _pnode->_data;
		}
		Self& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}
		Self operator++(int)
		{
			Self tmp(*this);
			_pnode = _pnode->_next;
			return tmp;
		}
		Self& operator--()
		{
			_pnode = _pnode->prev;
			return *this;
		}
		Self operator--(int)
		{
			Self tmp(*this);
			_pnode = _pnode->_prev;
			return tmp;
		}
		bool operator!=(const Self it)const
		{
			return _pnode != it._pnode;
		}
		bool operator==(const Self& it)const
		{
			return _pnode == it._pnode;
		}
	};

 其实不少同学可能会困惑,为什么要在迭代器中重载出->,这个不是我们在用结构体或者类中指针成员才用到的吗?

我们要明白list节点中可能存放的不是数据,也可能是存放指针一个结构体的指针。

下面我们来看代码理解:

	struct Pos
	{
		int _row;
		int _col;

		Pos(int row = 0, int col = 0)
			:_row(row)
			, _col(col)
		{}
	};

	void print_list(const list<Pos>& lt)
	{
		list<Pos>::const_iterator it = lt.begin();
		while (it != lt.end())
		{
			//it->_row++;

			cout << it->_row << ":" << it->_col << endl;

			++it;
		}
		cout << endl;
	}
	void test3()
	{
		list<Pos> lt;
		Pos p1(1, 1);
		lt.push_back(p1);
		lt.push_back(p1);
		lt.push_back(p1);
		lt.push_back(Pos(2, 2));
		lt.push_back(Pos(3, 3));

		// int* p  -> *p
		// Pos* p  -> p->
		list<Pos>::iterator it = lt.begin();
		//list<Pos>::iterator it2 = it;
		while (it != lt.end())
		{
			it->_row++;

			//cout << (&(*it))->_row << ":" << (*it)._col << endl;
			cout << it->_row << ":" << it->_col << endl;
			//cout << it.operator->()->_row << ":" << it->_col << endl;

			++it;
		}
		cout << endl;

		print_list(lt);
	}

 这里我们定义了一个Pos的类,他的功能就是记录row 和col,在定义一个函数print_list打印list中的做标,下面在我们的测试函数在插入一些数据。如果是在测试函数体内打印lt本来是非常复杂的如果没有重载迭代器的->.

这里理解: (&(*it))->_row?----->简单的来是就是要拿到这个it节点中的数据

如果我们要拿到Pos中的数据就只要用Pos创建一个变量p,p->row,就能拿到类中的数据,但是现在我们只有一个指向链表节点的迭代器,也就是只要我们*解引用it就能拿到节点中的数据,但是节点中的数据是一个类的,要能到类Pos的数据就要拿到类的地址,并用->指向结构体中变量的数据。

听起来是不是好晕,所以为了简化操作我们就在迭代器的类中封装了->.

	Ptr operator->()
		{
			return &_pnode->_data;//&这里是取地址,也就是说返回的指针
		}

迭代器失效问题 

我们都知道迭代器是用类封装好的里面有功能各异的成员函数,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代 器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响

3、list整体实现 

这里我们在整体实现的时候仍然采取分文件的做法,test.cpp用来包含所要的头文件,list.h用来实现list的主体内容。

test.cpp

#define  _CRT_SECURE_NO_WARNINGS

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

int main()
{
	pjb::test1();
	return 0;
}

list.h

#pragma once//防止头文件被多次包含


namespace pjb
{
	template<class T>
	struct list_node
	{
		list_node*  _next;
		list_node*  _prev;
		T _data;
		list_node(const T& x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
	};
	//定义迭代器
	template <class T,class Ref,class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref, Ptr> Self;
		node* _pnode;
		//初始化
		__list_iterator(node* p)
			:_pnode(p)
		{}

		Ptr operator->()
		{
			return &_pnode->_data;
		}
		Ref operator*()
		{
			return _pnode->_data;
		}
		Self& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}
		Self operator++(int)
		{
			Self tmp(*this);
			_pnode = _pnode->_next;
			return tmp;
		}
		Self& operator--()
		{
			_pnode = _pnode->prev;
			return *this;
		}
		Self operator--(int)
		{
			Self tmp(*this);
			_pnode = _pnode->_prev;
			return tmp;
		}
		bool operator!=(const Self it)const
		{
			return _pnode != it._pnode;
		}
		bool operator==(const Self& it)const
		{
			return _pnode == it._pnode;
		}
	};

	//定义lsit的类
	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		typedef __list_iterator<T, T&, T* >iterator;
		typedef __list_iterator<T, const T&,const T*> const_iterator;
		//初始化哨兵位的头
		void empty_initialize()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;
		}
		//构造函数
		list()
		{
			empty_initialize();
		}
		//析构函数
		~list()
		{
			clear();
			//清除头节点
			delete _head;
			_head = nullptr;
		}
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}
		template <class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			empty_initialize();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

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

		//交换
		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}
		//lt2(lt1)
		list(const list<T>& lt)
		{
			empty_initialize();
			list<T> tmp(lt.begin(), lt.end());
			swap(tmp);
		}
		//lt3 = lt1
		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}
		//删除
		iterator erase(iterator pos)
		{
			assert(pos != end());
			node* prev = pos._pnode->_prev;
			node* next = pos._pnode->_next;

			prev->_next = next;
			next->_prev = prev;
			delete pos._pnode;
			--_size;
			return iterator(next);
		}
		//插入
		iterator insert(iterator pos, const T& x)
		{
			//为插入申请新空间
			node* newnode = new node(x);
			node* cur = pos._pnode;//指向要插入位置的节点
			node* prev = cur->_prev;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
			return iterator(newnode);//返回新节点的地址
		}
		//尾插
		void push_back(const T& x)
		{
			insert(end(),x);
		}
		//头插
		void push_front(const T& x)
		{
			insert(begin(), x);
		}
		//尾删除
		void pop_back()
		{
			erase(--end());
		}
		bool empty()const
		{
			return _size == 0;
		}
		size_t size()const
		{
			return _size;
		}

	private:
		node* _head;
		size_t _size;
	};
	//简单测试
	void test1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}
}

这里我们看到模拟实现的时候,我们还写了一个测试案例,下面去验证一下

三、list和vector的对比

1、对比二者的优缺点

vector

Vector的优缺点

优点

缺点

下标支持随机访问

前面部分效率低O(N)

尾插尾删效率高

扩容有消耗,存在一定的空间浪费

Cpu高速缓存命中高

list 

list的优缺点

优点

缺点

按需申请空间,无需扩容

不支持随机访问

任意位置插入删除O(1)

Cpu高速缓存命中低

2、list和vector的排序效率 

这里我们要注意的是list有自己专门sort排序,而vector是用算法库中的排序,这是因为list的结构的特殊性,算法库中的不能够满足list的排序。

那二者那个效率更好呢?

测试10万个数据二者的排序时间的差异:

void test_op()
{
	srand(time(0));
	const int N = 100000;
	vector<int> v;
	v.reserve(N);

	list<int> lt;
	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		v.push_back(e);
		lt.push_back(e);
	}

	int begin1 = clock();
	//对v排序
	sort(v.begin(), v.end());
	int end1 = clock();
	int begin2 = clock();
	//对lt排序
	lt.sort();
	int end2 = clock();
	printf("vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}

int main()
{
	test_op();
	return 0;
}

 从上面来看vector的排序效率是远大于list的,所以我们一个尽量不要使用list的排序。

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

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

相关文章

Acrel-2000Z电力监控系统在某数据中心的应用-Susie 周

1、概述 随着网络和信息技术的快速发展&#xff0c;人们对“大数据”业务需求不断增长&#xff0c;为了满足日益增长的应用需求&#xff0c;数据的建设规模也在向超大型、园区级数据方向发展。通信、金融、商业等行业&#xff0c;面对未来数据业务的爆发式增长需求&#xff0c…

Go cobra 库学习

cobra既是一个用于创建强大现代CLI应用程序的库&#xff0c;也是一个生成应用程序和命令文件的程序。cobra被用在很多go语言的项目中&#xff0c;比如 Kubernetes、Docker、Istio、ETCD、Hugo、Github CLI等等 其实简单的来说&#xff0c;cobra就是一个自定义命令工具&#xff…

从IPC到分布式软总线的随笔

在Linux 系统中&#xff0c; 客观来说&#xff0c;缺乏相对开发者比较友好的进程间通信框架。谈到Linux上进程间通信&#xff0c;一般都会想起管道&#xff08;匿名、有名&#xff09;、信号/信号灯、共享内存、消息队列和socket。这些都是偏低层的技术&#xff0c;有没有方便开…

HummerRisk 入门3:开发手册

本文是 HummerRisk 的开发手册&#xff0c;介绍项目的结构及如何配置 HummerRisk 的开发环境和开发中的注意事项&#xff0c;快速参与到 HummerRisk 项目的开发中来。 一、项目结构 二、配置开发环境 1、环境准备 后端 HummerRisk 后端使用了 Java 语言的 Spring Boot 框架…

从今天起真正释放创造力 | Werner Vogels 在 re:Invent 2022带来多项开发者福音

对于开发者而言&#xff0c;成就感来自于每一次敲下代码后可实现的创造力&#xff0c;而不是把时间和精力消耗在写千篇一律又无法复用的“胶水”代码&#xff0c;或是在越来越复杂的软件栈面前&#xff0c;疲惫地写业务流程并尽量减少 Bug。 更加不堪的是&#xff0c;有时仅仅…

软件测试工程师涨薪攻略!3年如何达到30K!

1.软件测试如何实现涨薪 首先涨薪并不是从8000涨到9000这种涨薪&#xff0c;而是从8000涨到15K加到25K的涨薪。基本上三年之内就可以实现。 如果我们只是普通的有应届毕业生或者是普通本科那我们就只能从小公司开始慢慢往上走。 有些同学想去做测试&#xff0c;是希望能够日…

[附源码]计算机毕业设计基于springboot架构的博客平台设计

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【电力系统】基于两阶段鲁棒优化算法的微网多电源容量配置附matlab代码

​✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法…

01 初识HTML5

HTML5结构组成 HTML5主要是由标签组成的&#xff0c;如下代码就是HTML5的主要组成部分&#xff1a;<!DOCTYPE html> <!-- 文档声明标签&#xff0c;表示用html5解析 --> <html lang"zh-CN"> <!-- languangen 表示英文 “…

【保姆级·创建对象】如何通过Supplier创建对象

Spring创建对象的主要方式有 通过自定义BeanPostProcessor&#xff0c;生成代理对象InstantiationAwareBeanPostProcessor createBean() -> resolveBeforeInstantiation() 通过supplier创建对象 createBean() -> doCreateBean() -> createBeanInstance() -> obtai…

行业寒冬下逆势拿到50万offer,看看大牛是怎么做到的

年薪50万&#xff0c;这个薪水我以前想都不敢想&#xff0c;我一直以为月薪2万就是软件测试的天花板。当越来越多的同行拿到更高的薪水&#xff0c;我才明白&#xff0c;限制我薪水的不是行业天花板&#xff0c;而是我的技术实力。 每天陀螺一样两点一线的在家和公司之间往返&…

Nature文章使用认证Kamiya艾美捷抗胸腺嘧啶二聚体单抗方案

细胞内、外部环境中普遍存在的DNA损伤因素会破坏遗传信息的稳定性。紫外线损伤皮肤的机制之一是损伤细胞的DNA&#xff0c;形成“晒伤细胞”&#xff0c;诱发细胞内DNA产生丰富的变异&#xff0c;主要包括环丁烷嘧啶二聚体&#xff08;CPD&#xff0c;Cyclobutane pyrimidine d…

网络安全域内用户Hash获取方式

前言 在渗透测试的过程中&#xff0c;当我们已经是域管权限时&#xff0c;就可以实现提取所有域内用户的密码哈希以进行离线破解和分析&#xff0c;这是非常常见的一个操作&#xff0c;这些哈希值存储在域控制器(NTDS.DIT)中的数据库文件中&#xff0c;并带有一些其他信息&…

5-3:Spring整合Kafka

引入依赖 spring-kafka <!-- https://mvnrepository.com/artifact/org.springframework.kafka/spring-kafka --> <dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId>/*可以注释掉&#xff…

小程序webView页面转发后,进入页面空白

小程序webView页面&#xff0c;在点击右上角按钮分享后&#xff0c;进入分享的链接页面空白 重新进入页面后&#xff0c;页面空白。使用电脑打开之后报错提示如下 一、排查页面转发后&#xff0c;页面地址有没有解码 webview页面转发后&#xff0c;小程序会将url参数转码&…

Java并发-CompletableFuture的详解

目录 1 前言 2 常用方法 3 测试 3.1 runAsync&#xff1a;无返回值 和 SupplyAsync&#xff1a;有返回值 3.2 串行执行 3.3 任务3等待等任务1和任务2都执行完毕后执行 3. 4 任务3等待等任务1或者任务2执行完毕后执行 3.5 handleAsync 3.6 多任务执行 1 前言 Completable…

LabVIEW中将前面板置于所有桌面窗口的前面

LabVIEW中将前面板置于所有桌面窗口的前面 想将前面板窗口设置在所有桌面窗口的前面。前面板属性IsFrontmost&#xff08;如下图所示&#xff09;将前面板设置为仅位于所有 LabVIEW 窗口的前面。如何将前面板置于所有桌面窗口的前面&#xff1f; 解决方案 如果使用位于C:\WIN…

【JavaWeb】第七章 Tomcat

文章目录1、JavaWeb2、Web资源与Web服务器3、Tomcat的使用4、部署Web工程到Tomcat中5、工程和资源的访问6、IDEA集成Tomcat服务器7、创建动态web工程1、JavaWeb JavaWeb是指通过Java语言编写的可以通过浏览器访问的程序的总称。 请求&#xff1a; 客户端给服务器发送数据&…

java基于SSM的婚纱影楼管理系统-计算机毕业设计

开发环境 开发工具:IDEA /Eclipse 数据库:MYSQL5.7 应用服务:Tomcat7/Tomcat8 使用框架:SSM(springspringMVCmybatis)vue 项目介绍 婚姻是每个人人生中都非常重要的一个组成部分&#xff0c;它是一个新家庭的开始也是爱情的见证&#xff0c;所以很多人在结婚之前都会拍一套美…

数字先锋 | 农业农村部大数据公共平台基座上线,天翼云擎起乡村振兴新希望!

近日&#xff0c;由中国农业农村部大数据发展中心牵头&#xff0c;联合中国电信集团有限公司、人民数据管理&#xff08;北京&#xff09;有限公司、北京中农信达信息技术有限公司、北京农信通科技有限责任公司共同打造的“农业农村大数据公共平台基座”已正式上线&#xff0c;…