本文代码已同步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区别
vector和list都是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、总结对比
| 特点 | vector | list |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 内存结构 | 连续 | 不连续 |
| 随机访问 | 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) | 拷贝构造函数 |
我们依次来使用
在使用之前先实现一个打印容器的函数模板
cpp
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中的有效元素 |
我们来使用这些接口

四、迭代器特点
- 类型限制 :双向迭代器 ,只能
++和--移动,不支持+n、[]或比较大小。 - 失效规则 :极其稳定。插入和删除元素时,仅被删元素的迭代器失效,其他所有迭代器保持有效。
- 底层本质 :封装了链表节点的指针,移动迭代器就是移动
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中,插入一个节点之后,仅仅改变了指针的链接,相对位置并未发生改变,因此,list中insert之后迭代器不失效
接着来看erase:
在vector中,删除pos位置的元素之后,pos位置的位置失效;
在list中,删除pos节点之后,指向当前节点的迭代器同样失效,但其他位置的迭代器并无影响;
如果觉得有帮助,可以关注Github项目持续更新