C++-----list

本期我们来讲解list,有了string和vector的基础,我们学习起来会快很多

目录

list介绍

​编辑

list常用接口

insert

erase

reverse

sort

merge

unique

remove

splice

模拟实现

基础框架

构造函数

push_back

迭代器

常见问题

const迭代器

insert

erase

push和pop

size

析构和clear

拷贝构造

赋值

全部代码


本期内容需要比较扎实的基础

list介绍

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

简单来说,list就是带头双向循环列表

我们先简单看看如何使用

list常用接口

list的迭代器啦,构造函数,析构函数这些跟我们前面学的都是一样的,大家稍微看看文档即可,这里就不细讲了

不过list不一样的是它有头插头删,尾插尾删,这些和我们以前用C语言实现的是一样的

insert

我们从vector开始,insert里的参数都不是pos,而是迭代器了

我们之前使用vector时是可以使用迭代器+一个数的,而list是不行的,因为vector是数组,是连续的空间,而list是链表 ,是一个个节点

我们想在5的位置插入,是需要手动让迭代器走的,然后再插入

我们上面插入了1,2,3,4,5,如果我们想在数字3前面插入数据,是需要使用find的,但是我们看文档,list是没有提供find的,我们在vector里说过,find属于算法,通过迭代器来和算法联系起来,我们通过统一的方法,而不用关注容器底层是如何实现的

我们来看find的查找,它的循环条件是first!=last,而不是小于,我们用while来进行遍历,也使用的是begin!=end,而不是小于,因为后面节点的地址不一定比前面小

通过迭代器,不止是vector,list,即使是树也是可以查找的,因为都是迭代器,区间范围是左闭右开

比如我们在3前面插入30

并且由于是带头双向循环的原因,insert是不存在迭代器失效问题的

erase

虽然insert没有迭代器失效问题,但是erase是有的

节点都不存在了

insert以后,it不失效,erase以后,it会失效

如果我们要删掉所有的偶数呢?

是需要使用返回值的,和vector是一样的

返回的是刚刚被删除的元素的后一个

reverse

reverse是链表的逆置,其实有点多余了,因为算法库里也有

意义其实不大

sort

sort在库里面也有,那是否也是多余的呢?

这里编译报错了

sort编译报错了,原因是sort底层是快排,快排是需要三叔数取中的,会取到这个数的位置,链表是不适应这个场景的

我们仔细看算法库里sort,find,reverse这些,都是模板,我们仔细看参数名字,sort的是random,reverse的是bidirection,一个是随机的意思,一个是双向的意思,这些都是暗示,这些名字都不是随便起的

这里就和迭代器有关了,迭代器分为单向迭代器,双向迭代器和随机迭代器,单向迭代器只能++,双向可以++也可以- -,而随机不仅可以++和- -,还可以+和-

单链表的迭代器就是单向迭代器,双向链表,也就是list就是双向迭代器,还有map和set也是,vector和string是随机迭代器

find的迭代器是input,是所有迭代器都可以使用,这里涉及到继承,我们后面会讲

那我们该如何知道一个容器的迭代器属于什么呢?

其实文档里都有说明 ,比如list的迭代器是双向的

但迭代器的使用不是定死的,比如string,vector都可以使用reverse,因为随机迭代器可以看作特殊的双向迭代器,是可以兼容的,比如双向可以使用单向的

我们再回过头来看list的sort还有意义吗?有一点,但不多

如果要排序的话,我们应该使用vector,而不是list,在500w的数据量时,vector能比list快10倍

list的排序底层使用的是归并排序,所以list的sort的意义就是方便

我们要排序的话

可以这样做,把list的数据拷贝到vector,然后再拷贝回来

所以各位以后在排序时,尤其是数据量大时,不要用list的sort

merge

merge是两个链表的归并

不过归并前先排序一下

unique

unique去重的意思,不过去重前也是需要先排序的,不然效率太低了

remove

remove就是find加erase

找不到的话就什么也不做,找到就删除

splice

splice是转移,可以将a链表的节点拿下来转移到b链表

我们看第一个接口,是把x链表的所有值转移到当前链表(position)之前,第二个是转移x链表的i,第三个是转移一个区间

这里是调用第一个接口 ,是全部转移,此时mylist2是空的

这里调用第二个接口,我们把第二个链表的第二个数转移到第一个链表

这是第三个接口 ,我们把第二个链表的第二个位置开始全部转移

还可以自己转移自己

我们把第二个位置转移到第一个位置的前面

还有不要有重叠,像这样就死循环了

模拟实现

下面我们来进行模拟实现

首先我们先看看源代码

我们看到有一个void*类型

我们还找到了链表

还找到了成员变量

它的本质是这样的

我们还看到了无参的构造函数 ,初始化了一个节点

getnode是哨兵位节点

putnode下面我们发现了熟人,construct

这些我们了解一下即可

我们还可以找到pushback ,我们发现它调用了insert

我们就不再深入,各位感兴趣的话可以自己再看看,下面我们进入正题

基础框架

我们使用struct定义节点,使用class也可以,不过需要使用public,节点不是公有的使用起来会非常麻烦

补全一下,最后写成这样,看框住的地方,有人可能直接在private里写list_node* _head,这样是错误的,因为 忘记了<T>

构造函数

cpp 复制代码
        list()
		{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
		}

除了list里要写构造函数,节点里也要写一个,因为我们在插入节点时是需要创建的,而创建时调用构造函数即可

push_back

cpp 复制代码
        void push_back(const T& x)
		{
			Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

其实写起来和我们以前用C实现的是一样的

迭代器

首先,大家想一想我们的迭代器用Node*可以吗?

大家想一想,数组是连续的,我们解引用后就可以直接用,++就可以到下一个位置,但是链表不行,它不是连续的,即使当前是连续的,我们插入一个值,也会变成不连续的

我们当时日期对象++是如何到下一天的?是调用了一个函数,是需要运算符重载

我们看库里面,是这样实现的 ,我们要++的话,就是让node=node->next

深入看库对于现在的我们来说还是有点难度的,我们就不再往下看

所以我们最后迭代器是这样一个东西

现在需要写begin和end

我们遍历是这样写的

begin是第一个数据,end是最后一个数据的下一个位置

我们看库里面,库里面的node是我们的head,end返回了node,begin返回了node的next,和我们是一样的

迭代器是自定义类型,是节点指针,但因为底层原因,不是连续的,我们不能用原生的,不像之前的vector等等,C++可以用一个类来封装内置类型,然后重载运算符,比如++就是调operator++,就变成我们来控制了,就像C里面指针是不符合我们行为的,我们可以把它写的和指针一样,但是底层不一样

就像这样,再想想我们要写list的遍历,也是这样写,但是*it看起来一样,实际不一样,一个数组,一个链表,甚至一棵树,遍历都可以这样写,封装和运算符重载的力量是非常强大的

我们从应用的角度来实现,即从遍历这里开始写怎么写迭代器

我们补全一下__list_iterator,再看 begin,这里为什么可以这样返回呢?因为单参数的构造函数支持隐式类型转换,就像const char*可以转换为string一样

还可以这样写,本质是一样的

我们再想想解引用该怎么办

转换为调用operator*,返回节点里的值,因为出了作用域节点还在,val也还在,所以我们返回引用

同样的,我们还要写++,我们让node=node->next即可

我们还要写不等于

此时我们进行测试发现报错了,报错的是!=

原因是在我们的循环条件里,去调用operator!= ,然后我们的operator!=的参数我们给的是引用,调用了end,end返回的是iterator,是传值返回,传值返回的是head的拷贝,是临时对象,具有常性,所以我们在!=要加const

也就是这样

此时就没有问题了

我们上面实现了++,我们++有前置++,还有后置++,下面是后置++的实现

同样的,我们实现了不等于,也要实现等于

常见问题

下面再解释一下第一次学习的同学一些常见问题

为什么两个typedef不在一块,iterator是对外用的,所以放在public里,而Node是对内用的,我们不希望别人访问我们的节点,别人使用迭代器就可以了,所以不在一块

还有为什么我们起名是list_node,而不是node,迭代器也是,因为除了list,还有vector,树等等各种结构,他们都在std这个命名空间里面

就像我写的这个namespace bai,官方的std,它所有的都在这里面,所以不这样取名会存在命名冲突问题

还有一个疑惑,迭代器的本质是通过自定义类型封装,改变了原生指针的行为,达到我们的目的

再看一个问题,这里调用begin,begin的值是如何给it的? 是拷贝构造,这里不是赋值

我们现在的迭代器没有写拷贝构造,默认生成的拷贝构造对内置类型完成值拷贝,也就是浅拷贝,但是没有问题,我们要的就是浅拷贝

begin返回这个位置的迭代器,给it,我们就期望it里面的节点指针也是指向这个位置

但是两个对象指向同一个节点,为什么这里没有崩溃?

崩溃的核心原因在于析构函数多次释放,但是迭代器对象我们没写析构函数,原因是节点不是属于迭代器的,不需要迭代器来进行释放,我们只是借助迭代器来访问容器,不论数组,链表,树,我们都可以用同样的方式来访问容器,而不是管理容器,节点是由链表释放的

const迭代器

大家先想想这样设计const迭代器对吗?

迭代器模拟的是指针,指针有两种const指针

const迭代器模拟的是第一种指针的行为,第二个指针是不能++的,const在*后面,修饰的是指针本身,而第一个是修饰指向的内容不能修改,const迭代器是期望指向的内容不能修改

此时我们再看我们上面设计的,是不行的,我们模拟的是上面的ptr2,这样写出来的是const迭代器本身不能修改,我们就不能遍历,不能++了,因为++是非const,const是不能调用非const的,同样我们也不能把++变为const,因为++里要改变成员变量

我们怎么写才能控制指向的内容不能修改?

我们控制解引用即可 ,这样返回的就是const引用

此时就会有人把上面迭代器整个拷贝一份,然后改改名什么的,那样设计太冗余了

我们再看库里面,它加了两个模板参数

我们先在自己的迭代器这里加一个Ref,然后修改operator*的返回值类型

接着我们在list里就可以typedef const迭代器了

其实这样写也相当于我们写了两个类,大家想一想,vector<int>和vector<double>是两个类,是两个相似的类,有编译器通过模板生成

而我们这样写,通过模板参数,给T引用的时候,operator*返回的就是T引用,给const T引用时返回的就是const T引用,但是他们两个是两个类,给不同的模板参数就是不同的类

这样写的话,我们还要修改一些东西

我们要在迭代器类里写这样一个,我们习惯叫做self,就是自己的意思

接着我们把这些修改为self即可

还有别忘记写const的begin和end

我们继续看源码发现,除了重载operator*之外,还重装了这样一个,并且库里面是三个模板参数,我们现在只有两个

我们知道,指针除了*解引用,还有->解引用,*是取指针指向的数据,指向的对象,那->呢?

如果是一个结构体的指针,就需要->,迭代器是模拟指针的行为,什么时候模拟结构体指针呢?

operator->默认返回的是T*

我们先看这样一段代码,我们下面*it解引用是什么? 这段代码为什么不能运行?

因为*it是取里面的数据T,T是A,而A是自定义类型,这里是需要重载流插入的,这是第一种方法,如果我们不想重载,A里的成员变量a1,a2都不是私有的

我们是可以这样写的

此时迭代器是模拟的指向结构体的指针,如果这里类型是int,我们直接解引用,如果是自定义类型,是需要用箭头的

是可以这样写的

但是这里有个非常诡异的问题

首先,我们把operator->屏蔽掉的话这里是会报错的

但是,这个箭头调用是非常奇怪的,operator*是没问题的

如果是*it,这里就是it.operator*() ,然后operator*返回A&,A.a1,A.a2是没有问题的

而箭头是it.operator->(),返回的是A*,A*怎么去访问呢?

所以这里严格来说严格是it->->_a1,才是符合语法的,是两个箭头,第一个箭头的运算符重载,调用operator->返回A*,A*再加一个箭头才能访问

因为运算符重载要求可读性,所以编译器在这里是特殊处理的,省略了一个箭头

如果是const迭代器,operator->应该返回const T*

所以我们需要加第三个模板参数Ptr

然后用Ptr代替T*

普通迭代器传T*,const传const T*

我们上面实现的思路大家也发现了,我们要从核心的东西开始看,如果一上来我们就看到迭代器有三个参数,人都是懵的,是看不明白的,所以看不懂的东西我们可以先暂时放下,先看后面的,看它的核心,看它的功能,慢慢就可以知道了,我们看不懂的原因是当前站的高度太低了,很多东西是需要看好几遍的

另外这些不修改的我们最好再加上const

cpp 复制代码
        self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

再补充一下--

insert

cpp 复制代码
        void insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);
			prev->_next = newnode;
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
		}

insert非常简单,这里是可以体先出为什么我们节点使用的是结构体,而不是类,如果是类的话,会变得麻烦一点,我们是和库保持一致的,库里面也使用的是结构体,而且我们不用担心有人会写it._node这种代码,因为官方只规定了迭代器可以++这些,而没有规定底层,我们不确定这里是_node,还是Node,或者是_Node,即使我们去查了源码,也只是针对当前平台,换一个平台可能就不同了

库里面和我们是差不多的

erase

cpp 复制代码
        void 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;
		}

erase是需要先检查一下的,end是哨兵位,不能删除,另外,我们可以发现,这里是存在迭代器失效问题的,所以我们需要返回下一个位置,另外我们上面还看到了insert其实也有返回值,我们一块补充一下

cpp 复制代码
        iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);
			prev->_next = newnode;
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			return newnode;
		}
		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;
			return next;
		}

push和pop

cpp 复制代码
        void push_back(const T& x)
		{
			/*Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;*/
			insert(end(),x);
		}
		void push_front(const T& x)
		{
			insert(begin(), x);
		}
		void pop_back()
		{
			erase(--end());
		}
        void pop_front()
		{
			erase(begin());
		}

这些都没什么说的,有了insert和insert,我们都是可以复用的

size

cpp 复制代码
        size_t size()
		{
			size_t sz = 0;
			iterator it = begin();
			while (it != end())
			{
				++sz;
				++it;
			}
			return sz;
		}

我们可以这样遍历一遍,如果感觉这样不好,也可以加一个成员变量_size

这样写我们要修改一下insert和erase

++一下即可,其他地方不用变 ,同理,erase里--一下即可

还有初始化也要修改一下

析构和clear

cpp 复制代码
        ~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
			_size = 0;
		}

clear和析构是不一样的,clear是清理数据,清理还要释放空间,不过哨兵位是不清的,所以我们需要接受一下,it刚好接受下一个位置,最后处理一下size即可,析构直接复用即可

我们简单测试一下,没有问题

拷贝构造

cpp 复制代码
        list(const list<T>& lt)//拷贝构造
		{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;
			for (auto& e : lt)
			{
				push_back(e);
			}
		}

我们把数据一个一个的放进去即可

这里是有点冗余,我们提取一下公共部分,库里面也是这样写的

赋值

cpp 复制代码
        void swap(list<T>& lt)
		{
			std::swap(_head,lt._head);
			std::swap(_size, lt._size);
		}
		list<T>& operator=(list<T> lt)//赋值
		{
			swap(lt);
			return *this;
		}

和vector一样,我们直接用现代写法

测试一下也没有问题

仔细看我们和库里面的区别,库里面最开始是list&,我们是list<T>&

我们写的是类型,它写的是类名

对于拷贝构造和赋值,写类名是允许的,不过这样写有点降低可读性,我们这里不推荐和库一样

类模板在类里面使用,既可以写类名,也可以写类型,虽然语法上允许,不过我们最好保持统一写类型

全部代码

cpp 复制代码
#include<iostream>
#include<list>
#include<algorithm>
#include<assert.h>
using namespace std;
namespace bai
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;
		list_node(const T& val = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_val(val)
		{}
	};

	template<class T,class Ref,class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref,Ptr> self;
		Node* _node;
		__list_iterator(Node* node)
			:_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& it) const
		{
			return _node != it._node;
		}
		bool operator==(const self& it) const
		{
			return _node == it._node;
		}
	};
	
	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;

		//typedef const __list_iterator<T> const_iterator;//错误的

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

		const_iterator end() const
		{
			return _head;
			//return const_iterator(_head);
		}
		void empty_init()
		{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;
		}
		list()
		{
			empty_init();
		}
		//list(const list& lt)//拷贝构造
		list(const list<T>& lt)//拷贝构造
		{
			empty_init();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}
		void swap(list<T>& lt)
		{
			std::swap(_head,lt._head);
			std::swap(_size, lt._size);
		}
		
		//list& operator=(list lt)//赋值
		list<T>& operator=(list<T> lt)//赋值
		{
			swap(lt);
			return *this;
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
			_size = 0;
		}

		void push_back(const T& x)
		{
			/*Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;*/
			insert(end(),x);
		}
		void push_front(const T& x)
		{
			insert(begin(), x);
		}
		void pop_back()
		{
			erase(--end());
		}
		void pop_front()
		{
			erase(begin());
		}
		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);
			prev->_next = newnode;
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			++_size;
			return newnode;
		}
		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 next;
		}
		size_t size()
		{
			/*size_t sz = 0;
			iterator it = begin();
			while (it != end())
			{
				++sz;
				++it;
			}
			return sz;*/
			return _size;
		}
	private:
		Node* _head;
		size_t _size;
	};
}

以上即为本期全部内容,希望大家可以有所收获

如有错误,还请指正

相关推荐
wjs2024几秒前
Swift 数组
开发语言
南东山人28 分钟前
一文说清:C和C++混合编程
c语言·c++
stm 学习ing1 小时前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
湫ccc2 小时前
《Python基础》之字符串格式化输出
开发语言·python
mqiqe3 小时前
Python MySQL通过Binlog 获取变更记录 恢复数据
开发语言·python·mysql
AttackingLin3 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
Ysjt | 深3 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
ephemerals__3 小时前
【c++丨STL】list模拟实现(附源码)
开发语言·c++·list