本文代码已同步Github

一、为什么需要list

在上一篇文章中,我们学习了vector以及底层原理

在 C++ STL 中,vector 是我们最常使用的容器之一;
它底层采用动态数组实现,具有连续的内存空间,因此支持快速随机访问。

同样vector可以直接使用下标来访问元素,这也是vector的最大优势

但是,vector不是万能的,当我们需要频繁在容器中间进行插入和删除操作时
由于vector中的元素是连续存储,因此对于vector便会挪动数据,导致效率会明显下降

此时,我们便需要一种新的数据结构:

  • 不要求元素连续存储
  • 插入和删除元素时不需要大量移动数据

这就是链表(List)

std::list底层采用双向链表实现,每个元素都是一个单独的节点

插入或者删除仅需修改指针关系即可;


为什么会提供list?

不同的数据结构适用于不同的场景:

  • 如果需要大量查询、随机访问:
    • 选择 vector
  • 如果需要频繁插入、删除:
    • 选择 list

STL 并没有设计一个“万能容器”,而是提供了多种容器,让开发者根据实际需求进行选择。

vector 追求的是访问效率,而 list 追求的是修改效率。

因此,list 的存在并不是为了替代 vector,而是为了弥补 vector 在频繁插入和删除场景下的不足

二、list和vector区别

vectorlist都是STL中非常常用的容器,但由于底层结构不同,导致使用不同的场景;

简单来说:

vector适合随机访问,list适合插入与删除

1、底层结构不同

vector底层是一段连续的内存空间

因此可以通过下标随机进行访问;

时间复杂度为O(1)


list底层是一个一个独立的节点;

节点之间通过指针进行连接;

不能使用下标进行访问,一般使用迭代器进行遍历

想要访问第n个节点,就需要循环n次,一个一个节点向下找

时间复杂度为O(N)

2、插入和删除效率不同

vector中进行插入和删除时都需要挪动数据;

时间复杂度为O(1)

list中进行插入和删除时仅需修改指针指向即可,无需移动其他元素;

如果已经找到要插入或删除的位置,仅需**O(1)**时间即可

注意⚠️:如果没有找到位置,就需要先遍历链表,找到对应位置,这是**O(N)**级别

3、迭代器不同

我们仔细来看vector中的迭代器

在这里插入图片描述

发现:vector中是随机迭代器

再来看list

在这里插入图片描述

发现:list中是双向迭代器

只能**++/–**

4、总结对比

特点vectorlist
底层结构动态数组双向链表
内存结构连续不连续
随机访问O(1)O(n)
头部插入O(n)O(1)
中间插入O(n)O(1)(已有迭代器)
删除元素O(n)O(1)(已有迭代器)
空间利用
缓存友好
迭代器类型随机访问迭代器双向迭代器
支持下标×

三、常用接口

vector一样,我们先给出list接口文档:list的使用参考文档

1、默认成员函数

① 首先来看构造函数

在这里插入图片描述

整理出常用接口:

构造函数(constructor)接口说明
list()构造空的list
list(size_type n, const value_type& val = value_type())构造的list中包含n个值为val的元素
list(InputIterator first, InputIterator last)用[first, last)区间中的元素构造list
list(const list& x)拷贝构造函数

我们依次来使用

在使用之前先实现一个打印容器的函数模板

template<class T>
void print_container(const T& val)
{
	for (auto& e : val)
	{
		std::cout << e << " ";
	}
	std::cout << std::endl;
}

接着来测试试试

在这里插入图片描述


析构函数

在这里插入图片描述

析构函数就是在list对象生命周期结束时清理资源,自动调用;


赋值重载运算符

在这里插入图片描述

注意⚠️:两个已经存在的对象赋值才会调用赋值重载运算符;否则便会调用拷贝构造

来测试一下:

在这里插入图片描述

2、迭代器

在这里插入图片描述

整理出常用接口:

迭代器接口说明
begin + end返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rbegin + rend返回第一个元素的reverse_iterator,即end位置, 返回最后一个元素下一个位置的reverse_iterator,即begin位置

我们依次来使用

在这里插入图片描述

3、容量

在这里插入图片描述

整理出常用接口:

函数声明接口说明
empty检测list是否为空,是返回true,否则返回false
size返回list中有效节点的个数

来使用一下

在这里插入图片描述

4、元素访问

在这里插入图片描述

函数声明接口说明
front返回list的第一个节点中值的引用
back返回list的最后一个节点中值的引用

来测试一下

在这里插入图片描述

5、修改操作

在这里插入图片描述

整理出常用接口:

函数声明接口说明
push_front在list首元素前插入值为val的元素
pop_front删除list中第一个元素
push_back在list尾部插入值为val的元素
pop_back删除list中最后一个元素
insert在listposition位置中插入值为val的元素
erase删除listposition位置的元素
swap交换两个list中的元素
clear清空list中的有效元素

我们来使用这些接口

在这里插入图片描述

四、迭代器特点

  1. 类型限制双向迭代器,只能 ++-- 移动,不支持 +n[] 或比较大小。
  2. 失效规则极其稳定。插入和删除元素时,仅被删元素的迭代器失效,其他所有迭代器保持有效。
  3. 底层本质:封装了链表节点的指针,移动迭代器就是移动 node->next/prev

接着通过一张表格来和vector的迭代器来比较一下

对比维度std::list 迭代器std::vector 迭代器
类型(类别)双向迭代器随机访问迭代器
支持的运算++--==!=支持 ++--+n-n[]<><=>=
底层本质封装链表节点指针(Node*封装连续数组元素指针(T*
插入元素时失效永不失效若扩容则全部失效;未扩容则插入位置之后的失效
删除元素时失效仅被删除元素失效,其他迭代器保留被删元素及其之后的所有迭代器失效
内存布局非连续(节点分散于堆中)连续内存块
随机访问耗时O(n),必须逐节点移动O(1),直接指针偏移计算
对 end() 操作end() 是尾后哨兵,--end() 合法(前提非空)end() 是尾后指针,--end() 合法(前提非空)

五、特有接口

下面来看看list的特有接口

在这里插入图片描述

我们整理成表格

函数声明接口说明
splice将另一个list中的元素转移到当前list的指定位置
remove移除list中所有与给定值相等的元素
remove_if移除list中所有满足谓词条件的元素
unique移除list中连续重复的元素,只保留一个
merge合并两个已排序的list,合并后仍保持有序
sort对list中的元素进行排序(默认升序)
reverse反转list中元素的顺序

我们同样依次来使用

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

六、迭代器失效

最后来看迭代器失效问题:

首先来思考一下,对于insert是否有迭代器失效问题?

vector中,当在pos位置之前插入一个元素之后,由于相对位置的改变,我们认为迭代器失效了
而在list中,插入一个节点之后,仅仅改变了指针的链接,相对位置并未发生改变,因此,listinsert之后迭代器不失效

接着来看erase:

vector中,删除pos位置的元素之后,pos位置的位置失效;
list中,删除pos节点之后,指向当前节点的迭代器同样失效,但其他位置的迭代器并无影响;

如果觉得有帮助,可以关注Github项目持续更新

更多推荐