文章目录
-
- [一、从链表认识 list](#一、从链表认识 list)
- 二、不同构造方式
-
- [2.1 比较一些容易混淆的构造方式](#2.1 比较一些容易混淆的构造方式)
- [2.2 打印构造结果,验证复制是否独立](#2.2 打印构造结果,验证复制是否独立)
- 三、访问与遍历:迭代器是位置,不是下标
-
- [3.1 front() / back() 返回元素引用](#3.1 front() / back() 返回元素引用)
- [3.2 begin() / end() 表示半开区间](#3.2 begin() / end() 表示半开区间)
- [3.3 遍历方式](#3.3 遍历方式)
- 四、增删改查:先找位置,再执行操作
-
- [4.1 修改元素与修改序列](#4.1 修改元素与修改序列)
- [4.2 自定义类型插入:push_back 与 emplace_back](#4.2 自定义类型插入:push_back 与 emplace_back)
- 五、迭代器有效性与遍历删除
-
- [5.1 list 的稳定性来自节点,而不是元素数值](#5.1 list 的稳定性来自节点,而不是元素数值)
- [5.2 删除当前元素后,让返回值接管遍历位置](#5.2 删除当前元素后,让返回值接管遍历位置)
- 六、排序、去重、合并与节点转移
-
- [6.1 为什么 list 有成员 sort](#6.1 为什么 list 有成员 sort)
- [6.2 reverse 保持位置,不等于保持遍历顺序](#6.2 reverse 保持位置,不等于保持遍历顺序)
- [6.3 unique 相邻去重,完全去重前提是有序](#6.3 unique 相邻去重,完全去重前提是有序)
- [6.4 merge 的前提是两个序列按相同规则有序](#6.4 merge 的前提是两个序列按相同规则有序)
- [6.5 splice 转移节点:单个节点,或是一整段](#6.5 splice 转移节点:单个节点,或是一整段)
- [七、list 的相关功能成本](#七、list 的相关功能成本)
代码仓库:《list类测试与实现》
一、从链表认识 list
std::list<T> 是标准库的双向序列容器,包含头文件 <list>,T 表示元素类型。
带哨兵位双向循环链表的每个节点保存一个元素,并通过连接找到前后节点。各元素在物理空间存储时不要求位于连续地址,所以不能通过"起始地址加偏移"找到第几个元素,也不能用 [] 或 at() 按下标访问。
创建局部 list 对象后,容器负责创建和销毁元素、管理节点存储;对象离开作用域时自动清理。
二、不同构造方式
2.1 比较一些容易混淆的构造方式
和 string 与 vector一样,list 也有着丰富的构造方式:
| 写法 | 初始元素 | 要留意的地方 |
|---|---|---|
std::list<int> l; |
空 | 对象存在,但没有元素 |
std::list<int> l(5); |
五个 0 | 创建五个元素,不是预留五个位置 |
std::list<int> l(5, 2); |
五个 2 | 圆括号:数量与初值 |
std::list<int> l{5, 2}; |
5、2 | 花括号:这里选中元素列表构造 |
std::list<int> b(a); |
复制 a 的元素 | 节点独立管理 |
std::list<int> b = a; |
同样复制 a | 新对象的初始化,不是已有对象赋值 |
std::list<int> l(first, last); |
复制 [first,last) |
不包含 last 位置 |
整数的数量构造得到 0;自定义类型则按对应构造规则创建元素。区间构造必须提供有效范围,不能跨两个无关容器取起点和终点。
2.2 打印构造结果,验证复制是否独立
cpp
#include <iostream>
#include <list>
int main()
{
//使用循环给l1初始化
std::list<int> l1;
for (int i = 0; i < 5; ++i) l1.push_back(1);
//初始化l2 5个2,再拷贝构造给l3
std::list<int> l2(5, 2);
std::list<int> l3(l2);
//使用已有容器构造
int a[] = {3, 4, 5};
std::list<int> l4(a, a + 3);
//使用列表初始化
std::list<int> l5{5, 2};
std::list<int> l6(5);
std::cout << l6.size() << ' ' << l6.front() << std::endl;
//依次打印查看l3、l4、l5
for (int x : l3) std::cout << x << ' ';
std::cout << std::endl;
for (int x : l4) std::cout << x << ' ';
std::cout << std::endl;
for (int x : l5) std::cout << x << ' ';
std::cout << std::endl;
l3.front() = 9;
std::cout << l2.front() << ' ' << l3.front() << std::endl;
return 0;
}
运行示例:

std::list<int> l6(5);→ 创建5 个元素,值初始化为 0 ,输出5 0l2(5,2):5 个 2;l3(l2)拷贝构造 ,l3为独立链表,元素与l2相同l4(a, a+3):区间[a,a+3),数组{3,4,5}->3 4 5l5{5,2}:花括号初始化列表,两个元素:5、2l3.front() = 9;修改 l3 第一个节点,l2.front()仍然是2,l3.front()变成9
三、访问与遍历:迭代器是位置,不是下标
3.1 front() / back() 返回元素引用
非 const 容器的 front()、back() 返回的是可修改引用,因此 l.front() = 10 会改变元素。const 容器则返回只读引用。
cpp
std::list<int> l;
if (!l.empty())
l.back() = 10;
empty()判断是否为空;size()返回当前元素数量。list没有capacity()和reserve();节点随着元素增删来管理。
3.2 begin() / end() 表示半开区间
begin() 指向首元素,end() 是尾后位置。空链表中二者相等。

list 的迭代器是双向迭代器 :支持 *it、it->成员、++it、--it、相等比较。但它不支持随机跳转和大小关系比较,因此 it + 2、it1 - it2、it < end() 都不是正确写法。
3.3 遍历方式
与 vector 不同的是,list迭代器并不支持下标访问或者加减一个数值来得到元素位置。
cpp
#include <iostream>
#include <list>
int main()
{
std::list<int> l1;
l1.push_back(1);
l1.push_back(1);
l1.push_back(1);
l1.push_back(1);
l1.push_back(1);
for (auto n : l1)
{
std::cout << n << ' ';
}
std::cout << std::endl;
std::list<int> l2(5, 2);
std::list<int> l3(l2);
std::list<int>::iterator it = l3.begin();
while (it != l3.end())
{
std::cout << *it << ' ';
++it;
}
std::cout << std::endl;
return 0;
}
运行示例:

-
范围 for
for(auto n : l1) -
迭代器
while (it != l3.end ())list迭代器:双向迭代器 ,只支持++it/--it,不支持it + 2这种随机访问。- 判断条件:
it != l3.end(),不能用<list迭代器不支持大小比较,只能相等 / 不等判断。
| 容器 | 迭代器类型 | 支持运算 |
|---|---|---|
vector |
随机访问迭代器 | it < end()、it + n |
list |
双向迭代器 | 只能++it --it,禁止 < > 比较 |
四、增删改查:先找位置,再执行操作
4.1 修改元素与修改序列
| 操作 | 效果与返回值 | 边界 |
|---|---|---|
push_front(x) / push_back(x) |
头插 / 尾插;返回 void |
空链表也可使用 |
pop_front() / pop_back() |
删除首 / 尾元素;返回 void |
必须非空 |
insert(pos,x) |
在 pos 前插入,返回新元素迭代器 | pos 可为 end |
insert(pos,n,x) |
在 pos 前插入 n 个 x | 注意 n 是数量 |
insert(pos,first,last) |
插入指定来源范围的副本 | 来源范围有效,不能来自同一个目标 list |
erase(pos) |
删除 pos,返回后继 |
pos 必须指向本容器元素,不能是 end |
erase(first,last) |
删除 [first,last),返回后续位置 |
空范围可删除;范围必须有效 |
clear() |
删除全部元素 | 空容器也可调用 |
resize(n,x) |
缩小时删尾部,增大时追加 x | 无 x 时按默认规则初始化新元素 |
assign(n,x) / assign(first,last) |
用新内容替换全部旧内容 | 区间版本不使用本容器自身迭代器作为来源 |
swap(other) |
交换两个容器的内容 | 不是逐个复制元素 |
4.2 自定义类型插入:push_back 与 emplace_back
cpp
#include <iostream>
#include <list>
struct A
{
int _a1;
int _a2;
A(int a1 = 0, int a2 = 1) : _a1(a1), _a2(a2) {}
};
int main()
{
std::list<A> l;
A a(1, 10);
l.push_back(a);
l.push_back(A(2, 20));
l.emplace_back(3, 30);
for (auto it = l.begin(); it != l.end(); ++it)
std::cout << it->_a1 << ' ' << it->_a2 << std::endl;
}
push_back 只接收一个元素对象;而emplace_back(3,30) 会把参数用于在节点中构造 A。
这里只需掌握调用层面的差别,不展开底层参数转发。
五、迭代器有效性与遍历删除
5.1 list 的稳定性来自节点,而不是元素数值
- 插入一个节点,原节点无需整体搬移,已有元素的迭代器和引用也保持有效。
- 删除一个节点,只让指向被删元素的迭代器、引用和指针失效。但即使后来在相同地址新建节点,也不能据此继续使用旧迭代器。

| 操作 | 指向元素的迭代器 / 引用 / 指针 |
|---|---|
插入、头尾插、emplace |
原有元素的位置保持有效 |
erase、pop、remove、unique |
指向被删除元素的失效,其余保留 |
clear |
所有元素位置失效 |
resize |
增大保留原有位置;缩小使被删尾部位置失效 |
成员sort、成员 reverse |
元素位置保持有效,遍历顺序可改变 |
splice、merge |
被转移元素的位置仍有效,但归属变为目标容器 |
swap |
元素位置仍有效,但元素归属交换 |
assign、复制赋值 |
不依赖操作前的元素位置,完成后重新获取 |
5.2 删除当前元素后,让返回值接管遍历位置
cpp
#include <iostream>
#include <list>
int main()
{
std::list<int> l{1, 2, 3, 4, 5, 6};
//取list开头位置迭代器
auto saved = l.begin();
//取开头迭代器地址
int* address = &*saved;
//观察插入删除开头元素后迭代器地址是否变化
l.insert(saved, 0);
std::cout << (*saved == 1) << ' ' << (&*saved == address) << std::endl;
l.erase(l.begin());
std::cout << (*saved == 1) << ' ' << (&*saved == address) << std::endl;
auto it = l.begin();
while (it != l.end())
{
//删除list中的偶数
if (*it % 2 == 0)
{
it = l.erase(it);
}
else ++it;
}
//遍历打印此时list元素
for (int x : l) std::cout << x << ' ';
std::cout << std::endl;
//再次观察此时开头元素迭代器地址变化
std::cout << *saved << ' ' << (&*saved == address) << std::endl;
return 0;
}
运行示例:

saved = l.begin()指向元素1,address保存1的内存地址。
l.insert(saved, 0);在 1 前面插入 0
list 插入不失效原有迭代器
saved依旧指向节点1,节点内存地址没变,所以*saved==1、&*saved == address,输出1 1。
l.erase(l.begin());删除链表最前面的0。
删除的是另一个节点 0 ,不是 saved 指向的节点 1。
saved迭代器依旧有效,节点内存没有动,输出1 1。
erase只会让被删掉的那个迭代器失效 ,别的迭代器完好。这里 erase 的是begin()(节点 0),不是saved(节点 1)。
while循环删除偶数
原链表:
1,2,3,4,5,6→ 删除偶数后剩下:1 3 5。
- 最后打印
saved,它依旧指向节点 1,地址不变,输出1 1。

六、排序、去重、合并与节点转移
6.1 为什么 list 有成员 sort
算法标准库中的std::sort 要求随机访问迭代器,不能用于 list。需要对 list 元素排序时应使用 l.sort(),默认按升序排列;需要降序可写 l.sort(std::greater<int>()),包含 <functional>。
std::greater<int>()生成一个比较对象,判断左边是否大于右边。暂时把它理解成排序规则即可。
成员 sort 是稳定排序,比较意义下相等的元素保持原相对顺序,比较次数为 O(N log N)。
6.2 reverse 保持位置,不等于保持遍历顺序
l.reverse() 反转序列。已有元素迭代器继续关联原元素。
std::reverse(l.begin(),l.end()) 也可用于 list,因为这个算法只要求双向迭代器,但它通过交换元素值工作。因此一个有效迭代器仍指向原来的元素对象,读到的值却可能改变。
| 写法 | 做什么 | 迭代器有效性 | *it 的值 |
|---|---|---|---|
l.reverse() |
调换链表指针,节点不动 | 迭代器有效,指向原元素 | *it不变 |
std::reverse(l.begin(),l.end()) |
交换元素的值 | 迭代器有效 | *it被改写 |
两种写法迭代器都有效,但语义不一样。
6.3 unique 相邻去重,完全去重前提是有序

对 1 1 2 1 1 3 执行 unique 得到 1 2 1 3:两段连续的 1 各保留一个,分隔开的 1 不会合并。这样可以尽量可能保证原链表中的元素顺序不被打乱。
如果需求是不关心原顺序、相同值全局只留一个,可以先使用 sort 再 unique,把相同值聚到一起之后再去重。
6.4 merge 的前提是两个序列按相同规则有序
first.merge(second) 是把 second 的节点并入 first,然后得到有序序列,执行完后 second 变空;这不是简单把 second 接到尾部,而是把两个有序链表合并成一个有序链表。
合并保持元素迭代器有效,被转移的元素现在属于 first。默认合并不会把重复值自动去掉。
cpp
#include <algorithm>
#include <functional>
#include <iostream>
#include <list>
void print(const std::list<int>& l)
{
for (int x : l) std::cout << x << ' ';
std::cout << std::endl;
}
int main()
{
//初始化list
std::list<int> l{ 1, 1, 2, 1, 1, 3 };
//未排序去重
l.unique();
print(l);
//排序后去重
l.sort();
l.unique();
print(l);
//成员sort默认升序,使用仿函数改为降序
l.sort(std::greater<int>());
print(l);
auto it = l.begin();
int* address = &*it;
//反转链表后观察开头元素迭代器地址变化
l.reverse();
std::cout << *it << ' ' << (&*it == address) << std::endl;
std::reverse(l.begin(), l.end());
std::cout << *it << ' ' << (&*it == address) << std::endl;
//初始化两个相同规则的有序链表,合并后打印情况
std::list<int> first{ 1, 3, 5 };
std::list<int> second{ 2, 4, 6 };
first.merge(second);
print(first);
std::cout << second.empty() << std::endl;
}
运行结果:

使用 list::unique() 原序列:1 1 2 1 1 3,只消除相邻的重复,输出:1 2 1 3。
全局去重,先sort(), 再unique(),排序后变成 1 1 2 3,去重后输出:1 2 3;
使用 list::merge() 合并两个有序链表,1 3 5 + 2 4 6 -> 1 2 3 4 5 6 。
text
原始 {1,1,2,1,1,3}
l.unique(); → 1 2 1 3
l.sort();l.unique() → 1 2 3
l.sort(greater<int>()) → 3 2 1
l.reverse() → 1 2 3
std::reverse(...) → 3 2 1
merge之后
first → 1 2 3 4 5 6
second.empty() → 1(true)
6.5 splice 转移节点:单个节点,或是一整段
| 写法 | 转移内容 |
|---|---|
a.splice(pos,b) |
b 的全部元素,b 变空 |
a.splice(pos,b,it) |
b 中 it 指向的一个元素 |
a.splice(pos,b,first,last) |
b 的 [first,last) 范围 |
它们都插到 pos 前,不复制元素,也不要求序列是否有序。

cpp
#include <algorithm>
#include <iostream>
#include <list>
void print(const std::list<int>& l)
{
for (int x : l) std::cout << x << ' ';
std::cout << std::endl;
}
int main()
{
//初始化两个有序链表
std::list<int> first{1, 2, 3};
std::list<int> second{10, 20};
//取second开头元素迭代器与其地址
auto moved = second.begin();
int* address = &*moved;
auto pos = first.begin();
++pos;
//将second合并到first的pos前
first.splice(pos, second);
print(first);
//输出观察此时second是否为空以及迭代器变化
std::cout << second.empty() << ' ' << (&*moved == address) << std::endl;
//对自身进行合并(元素位置调换)
auto it = std::find(first.begin(), first.end(), 3);
if (it != first.end())
{
first.splice(first.begin(), first, it);
}
print(first);
// moved 现已属于 first,应该用 first 的范围边界操作它。
auto next = first.erase(moved);
print(first);
}
运行示例:

first.splice(pos,second)
把second全部节点转移 到first的pos迭代器前面;转移完成后second变成空。
节点不拷贝,只是改指针 ;原来
second上的迭代器moved依旧有效 ,只是现在归属first容器。
splice允许源容器与目标容器是同一个list,直接把本list内部某个节点剪切,挪到别的位置,不需要拷贝,效率极高。- 虽然
moved原本来自second,但节点已经转移到first,因此可以传给first.erase(moved);
七、list 的相关功能成本
list 的成本有许多和 vector 不同的地方,实际还需要根据具体场景选择使用。
| 工作 | list 的成本与条件 |
|---|---|
| 已知有效位置,插入 / 删除一个元素 | 常数复杂度,不搬移其余元素;仍有构造析构和分配释放的实际成本 |
| 查找某值,再删除它 | 查找 O(N),删除本身 O(1) |
| 访问第 k 个元素 | 逐步遍历,不能随机访问 |
单节点 splice / 整表 splice |
常数复杂度 |
跨容器区间 splice |
对转移数量线性;同容器区间转移可为常数复杂度 |
- 与
vector的有价值区别集中在这里:后者连续存储、支持随机访问,物理空间连续,高速缓存利用率高; list提供稳定节点位置和节点转移,按需申请和释放空间,但每个节点额外保存链接,也常需要独立分配。