C++STL之list容器


一:list特性

  • list为带哨兵位双向循环链表,支持任意位置的插入和删除。

  • 与(array,vector,deque)相比,list的移除元素效率更高。

  • 最大缺陷是不支持[]重载,不支持随机访问,只能通过迭代器进行线性开销的迭代。

二:list的排序

list无法使用算法库中的sort排序,算法库的sort底层是快排,需要三数取中,需要传入随机访问迭代器,所以list不适用。但是list的类域中自身提供了一种sort排序,底层是归并排序

通过图片分析,list的排序比vector调用算法库中的排序效率低,甚至比先将list数据拷贝给vector,再调用算法库的排序的效率还要低。

三:迭代器

1:迭代器失效

  • erase迭代器失效

  • insert迭代器不失效

2:迭代器种类

  • 单向迭代器,只能++,如单链表和哈希表。

  • 双向迭代器,支持++和--,如双向循环链表

  • 随机访问迭代器,支持++--和+-,如vector和string。

4:迭代器的价值

  • 使用封装,不能暴露底层的实现细节

  • 提供统一的访问方式,降低使用成本,这也是STL设计巧妙之处。

5:模拟实现

5.1:原生指针迭代器和封装迭代器类

  • vector和string在物理空间上是连续的,我们之前模拟实现的时候,采用typedef将原生指针定义为迭代器,vector和string是双向迭代器,迭代器的++和--都是跨过一个对象的大小,也就是T的大小。因为在物理空间连续,所以可以直接用原生指针。

  • 但是list在物理空间上不连续,如果用原生指针,因为不清楚前后节点在物理空间上谁是前谁是后,所以用原生的++--就无法访问到下一个节点,因此我们可以使用封装+运算符重载实现迭代器。

对于每一个节点,在C语言的时候是用struct,在这里因为不知道节点是该给公共权限还是私有,所以也直接设计成struct。

迭代器代码

迭代器模板中的Ref和Ptr后续讲解。迭代器类中设计好相关运算符的重载即可。

5.2:list类初步代码

5.3:const迭代器

const迭代器错误写法

如果给it迭代器添加const权限,那么it就无法更改,也就是it无法++--,这样显然不行。

因此我们可以考虑给迭代器类模板添加第二个参数。在list类模板中将普通迭代器和const迭代器使用同一个类模板创建2种类型,也就是第二个参数分别传普通的T和const T。因为const迭代器无法修改迭代器所指向的内容,因此传引用没有拷贝开销,所以第二个参数分别传T&和const T&。

因为多了一个参数,比如设计*重载的时候,返回值需要设置为__list_iterator<T, Ref>&,这样名字过长,因此我们可以这样。(Ptr下面再讲)

这里非const和const迭代器采用添加Ref参数的好处是,如果不这样写,就需要在迭代器类模板中分别实现每个函数的const和非const版本,这样一来代码重复量太大,不美观。

5.4:->重载

list类模板中,T是对象的类型,如果T是struct类型,想要通过迭代器解引用访问T对象的数据,就必须在迭代器类中提供->重载。

这里使用Ptr,是考虑有const和非const的迭代器,在list中我们就可以这样传参。

因为如果访问struct的成员,需要使用->,而struct指针可以做到,因此第三个参数要设计成指针形式,对于非const传T*,对于const传const T*,因此在迭代器的类模板中就可以添加第三个参数Ptr,表示指针,那么->重载的返回值就可以为Ptr。

我们一般可能遇到这种情景,为什么->这样重载就可以访问到数据呢?

it->我们知道应该是it.operator->(),这样的返回值是Pos*,Pos*_row这如何访问数据?

实际上,这里it->_row的本来面貌是it->->_row,这样就容易懂了,Pos*->_row就是结构体指针访问到数据,而编译器为了可读性,在这里省略了一个->,因此这里比较难懂。

5.5:模拟总代码

5.6:拷贝构造现代写法

和前面一样,直接 使用算法库的swap是一个深拷贝,代价太大,所以我们可以换个思路使用算法库的swap交换头节点即可,因为list只要头结点知道就可以访问下面的数据。

拷贝构造比如lt1(lt2),目的是将lt2的内容给lt1,不能改变lt2的内容,因此需要传const&

拷贝构造不能直接传值传参,必须用传引用传参,否则无限递归。

而如果通过临时对象构造一个lt2,可以再设计一个前后迭代器的构造函数,一开始设计的时候是没有empty这个函数的,因为如果将list初始化的代码直接放在构造函数中,这里我们设计拷贝构造的时候,lt1对象还没有实例化,仍然是一个类,所以不会去调用构造函数,所以头节点就是一个随机值。所以如果将随机值交换给temp,temp出了当前函数作用域后自动调用析构,析构的就是随机值,因此我们需要将初始化的代码单独使用一个empty函数。

5.7:=重载现代写法

和之前一样,用传值传参,不改变形参的特性。

四:list和vector的区别

  • 底层结构:vector是动态顺序表,连续空间,list是带头双向循环链表,物理空间不连续。

  • 随机访问:vector支持[]重载随机访问,效率O(1),list不支持随机访问,访问效率O(N)

  • 插入和删除:vector任意位置插入和删除的效率比较低(尤其是前中部分元素),需要挪动数据,时间复杂度为O(N),插入的时候需要增容,增容是异地扩容,拷贝元素,释放旧空间,效率更低。list任意位置插入和删除元素效率高,效率为O(1),不需要挪动数据。

  • 空间利用率:vector底层为连续空间,不易造成内存碎片,空间利用率高,高速缓存命中率高,list底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,高速缓存命中率低

  • 迭代器:vector是原生态指针,list采用封装+函数重载。

  • 迭代器失效:vector的insert可能导致扩容,迭代器失效,erase也会导致当前pos位置的迭代器失效。list的insert迭代器不会失效,erase导致当前迭代器失效。

  • 使用场景:vector适用于高效存储,随机访问,不关心插入删除效率。list适用于大量插入和删除操作,不关心随机访问。

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不熬夜不抽烟不喝酒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值