【C++】list的模拟实现

list与vector的对比

我们在通过对vector与list的文档的阅读已经理解,基本对list与vector有了大致的理解,vector与list都是STL中非常重要的序列式容器,由于俩个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下表:

对比 vector list
底层结构 动态顺序表,一段连续空间 带头节点的双向循环链表
随机访问 支持随机访问,访问某个元素效率O(1) 不支持随机访问,访问某个元素效率O(N)
插入与删除 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时又可能需要增容。(增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低) 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1)
空间利用率 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器 原生态指针 对原生态指针(节点指针)进行封装
迭代器失效 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来的迭代器失效,删除时,当前迭代器需要重新赋值否则会失效。 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响。
使用场景 需要高效存储,支持随机访问,不关心插入删除效率 大量插入和删除操作,不关心随机访问。

【总结】:

  • vector的优缺点:
    缺点1.头部和中部插入删除效率低,需要挪动数据,时间复杂度为O(N)。
    缺点2.插入数据空间一般不需要增容,当容量不足时需要扩容,开辟新空间,拷贝数据,释放就空间。
    优点1.支持下标的随机访问,从而可以很好的支持排序,二分查找,堆算法等等。

  • list的优缺点:
    优点1.list头部、中部插入不需要挪动数据,效率高,时间复杂度为O(1)。
    优点2.list插入数据是新增节点,不需要扩容。
    缺点1.不支持随机访问。

STL中的list底层其实就是带哨兵位循环双向链表。

list的模拟实现

list的三个基本函数类

list本质上是一个带哨兵位的双向循环链表:
在模拟实现list时,需要注意三个类:
1.模拟实现节点类。
2.模拟实现迭代器的类。
3.模拟list主要功能的类
list类的模拟实现主要功能的类需要建立在其他俩个类已经实现的前提之上。

【注意】关于struct与class在C++中的区别:
在C++中,struct与class的唯一区别就是默认访问限定符(即不写public、private、protected时的默认限定符)不同,struct默认为公有public、而class默认为私有private。
在一般情况下,成员有私有也有公有,使用class;成员中全书公有使用struct。

在下面的模拟实现的过程中,节点类与迭代器类都是由struct,因为struct中的成员函数默认为公有。

list的节点类的实现

由于list的本质是带哨兵位的双向循环链表,所以其每一个节点都需要保证有下列成员:
1.前驱指针
2.后继指针
3.data数据
同时,在实现这个类时,需要对其的成员进行初始化。

	template<class T>
	struct _list_node
	{
   
		//成员变量
		_list_node<T>* _next;
		_list_node<T>* _prev;
		T _data;
		//构造
		_list_node(const T& val = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_data(val)
		{
   }
	};

【注意】什么是_list_node< T >和_list_node?
这里使用一个类模板来定义里面的变量,类模板的类名是_list_node,这个不是类型;而_list_node< T >是真正的类型,就是说,使用类模板定义变量时必须指定对应的类型。

list的迭代器类的实现

以前在学习string与vetcor的时候,迭代器是不需要封装的,这是因为string与vector的物理空间是连续的,可以直接使用。
list的底层是带哨兵位的双向循环链表,而链表的物理空间是不连续的,是通过节点的指针顺次链接。

  • 基本框架
	template<class T, class Ref, class Ptr>
	struct _list_iterator
	{
   
		typedef _list_node<T> Node;
		typedef _list_iterator<T, Ref, Ptr> self;
		Node* _node;
	};

【迭代器类模板为什么存在三个参数】:
如果只是普通迭代器,仅存在一个class T模板参数即可,但是由于存在const限制的迭代器,需要增添俩个迭代器满足实现。俩个模板参数(Ref< reference引用 >,Ptr< pointer指针 >),这俩个参数模板的目的也是为例传指针与引用,目的后续讲解。

【为什么存在迭代器类】:
迭代器类就是通过封装将迭代器进行了包装,迭代器类就一个节点的指针变量_node,但是由于我们在使用迭代器的时候,会对迭代器进行运算符重载++,- -等操作,而这些操作在链表上普通迭代器是不会实现的。

【指针与迭代器的区别】:
指针与普通的迭代器是没有区别的,但是指针与被封装的迭代器区别就是,当它们都指向同一个节点是,在物理内存中它们都会指向这个节点的地址,但是在进行迭代器操作时会出现不同,指针在进行++时,会指向下一个地址,而迭代器会调用其迭代器的运算符重载函数,其下一个地址是可以被我们所操控的。

		//构造
		_list_iterator(Node* node)
			:_node(node)
		{
   }

设置一个迭代器的构造函数,只需要在初始化列表设置一个指针构造。

		//解引用
		Ref operator*()
		{
   
			return _node->_date;
		}

设置一个迭代器的解引用操作符运算符构造函数。
【返回值为什么是Ref】:
Ref是模板参数,因为迭代器类的模板参数Ref传入的要么是T& ,要么是constT& ,就是为了const迭代器和普迭代器的同时实现,其底层实现就是如此,就是为了一个只读,一个可读可写。

		Ptr operator->()
		{
   
			return &_node->_date;
		}

设置一个operator->()函数,利用operator->直接访问类的成员变量。(Ptr是迭代器的模板参数,用来作为T* 或者 const T*)

【注意】本质上,it->返回的是T*,只有->俩次才会访问T中的成员变量,但是编译器将其中的过程优化过一次。

		//前置++
		self& operator++()
		{
   
			_node = _node->next;
			return *this;
		}
		//后置++
		self operator++(int)
		{
   
			self tmp(*this);
			_node = _node->next;
			return tmp;
		}
		//前置--
		self& operator--()
		{
   
			_node = _node->prev
评论 33
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值