一、list的介绍和使用
1. list 的介绍
list 是 C++ STL 中的一种容器,底层采用环状双向链表 结构存储数据。与 vector 不同,list 并不支持随机访问,但其在任意位置的插入和删除操作都非常高效。
主要特点:
-
底层结构:环状双向链表
-
元素存储:节点独立,非连续内存
-
迭代器类型:双向迭代器
-
支持正向和反向遍历
-
适用于频繁插入/删除、不关心随机访问的场景
cpp
#include <list>
#include <algorithm>
list<int> mylist;
auto it = find(mylist.begin(), mylist.end(), 3);
2. list 的使用(常用接口)
2.1 list 的构造
| 构造函数 | 说明 |
|---|---|
list() |
构造一个空的 list |
list(size_type n, const value_type& val = value_type()) |
构造一个包含 n 个值为 val 的元素的 list |
list(const list& x) |
拷贝构造函数 |
list(InputIterator first, InputIterator last) |
用 [first, last) 区间中的元素构造 list |
cpp
list<int> l1; // 空list
list<int> l2(5, 10); // 5个10
list<int> l3(l2); // 拷贝构造
list<int> l4(l2.begin(), l2.end()); // 迭代器区间构造
2.2 list 迭代器的使用
迭代器可理解为指向链表节点的指针,支持移动和访问。
| 函数声明 | 说明 |
|---|---|
begin() / end() |
返回第一个元素的正向迭代器 / 返回最后一个元素下一个位置的正向迭代器 |
rbegin() / rend() |
返回最后一个元素的反向迭代器 / 返回第一个元素前一个位置的反向迭代器 |
注意:
begin()/end():正向迭代器,++操作指向下一个节点
rbegin()/rend():反向迭代器,++操作指向前一个节点
cpp
list<int> lst = {1, 2, 3, 4, 5};
// 正向遍历
for(auto it = lst.begin(); it != lst.end(); ++it) {
cout << *it << " ";
}
// 反向遍历
for(auto it = lst.rbegin(); it != lst.rend(); ++it) {
cout << *it << " ";
}
2.3 list 容量相关接口
| 函数声明 | 说明 |
|---|---|
empty() |
判断 list 是否为空 |
size() |
返回 list 中有效元素的个数 |
cpp
list<int> lst;
cout << lst.empty() << endl; // 1
lst.push_back(1);
cout << lst.size() << endl; // 1
2.4 list 元素访问
| 函数声明 | 说明 |
|---|---|
front() |
返回第一个元素的引用 |
back() |
返回最后一个元素的引用 |
⚠️ 注意 :使用
front()和back()前需确保list非空,否则行为未定义。
cpp
list<int> lst = {10, 20, 30, 40, 50};
cout << "第一个元素: " << lst.front() << endl; // 10
cout << "最后一个元素: " << lst.back() << endl; // 50
// 修改第一个元素
lst.front() = 100;
cout << "修改后第一个元素: " << lst.front() << endl; // 100
2.5 list 插入操作
| 函数声明 | 说明 |
|---|---|
push_front(const value_type& val) |
在链表头部插入元素 |
pop_front() |
删除头部元素 |
push_back(const value_type& val) |
在链表尾部插入元素 |
pop_back() |
删除尾部元素 |
insert(iterator pos, const value_type& val) |
在指定位置前插入元素 |
insert(iterator pos, size_type n, const value_type& val) |
在指定位置前插入 n 个相同元素 |
insert(iterator pos, InputIterator first, InputIterator last) |
在指定位置前插入迭代器区间内的元素 |
cpp
list<int> lst = {1, 2, 3};
// 头尾插入删除
lst.push_front(0); // {0, 1, 2, 3}
lst.push_back(4); // {0, 1, 2, 3, 4}
lst.pop_front(); // {1, 2, 3, 4}
lst.pop_back(); // {1, 2, 3}
// insert 操作
auto it = lst.begin();
++it; // 指向第二个元素(值为2)
lst.insert(it, 99); // {1, 99, 2, 3}
lst.insert(lst.begin(), 2, 88); // {88, 88, 1, 99, 2, 3}
list<int> other = {100, 200};
lst.insert(lst.end(), other.begin(), other.end()); // {88, 88, 1, 99, 2, 3, 100, 200}
2.6 list 删除操作
| 函数声明 | 说明 |
|---|---|
erase(iterator pos) |
删除指定位置的元素,返回下一个位置的迭代器 |
erase(iterator first, iterator last) |
删除 [first, last) 区间内的元素,返回最后一个被删除元素的下一个位置 |
clear() |
清空所有元素 |
remove(const value_type& val) |
删除所有值为 val 的元素 |
remove_if(UnaryPredicate pred) |
删除所有满足条件的元素(谓词返回 true) |
cpp
list<int> lst = {1, 2, 3, 2, 4, 2, 5};
// 按位置删除
auto it = lst.begin();
++it; // 指向第二个元素(值为2)
lst.erase(it); // 删除该元素 → {1, 3, 2, 4, 2, 5}
// 区间删除
auto first = lst.begin();
auto last = lst.begin();
++last; ++last; // first 指向1,last指向2(第二个2)
lst.erase(first, last); // 删除1和3 → {2, 4, 2, 5}
// 按值删除
lst.remove(2); // 删除所有值为2的元素 → {4, 5}
// 条件删除(删除所有偶数)
lst.push_back(6); // {4, 5, 6}
lst.remove_if([](int n) { return n % 2 == 0; }); // 删除4和6 → {5}
// 清空所有元素
lst.clear(); // {}
2.7 其他常用操作
| 函数声明 | 说明 |
|---|---|
resize(size_type n, value_type val = value_type()) |
调整链表大小为 n,超出部分删除,不足部分用 val 填充 |
assign(size_type n, const value_type& val) |
用 n 个 val 替换当前内容 |
assign(InputIterator first, InputIterator last) |
用迭代器区间内的元素替换当前内容 |
swap(list& x) |
交换两个 list 的内容 |
reverse() |
反转链表元素顺序 |
unique() |
删除链表中相邻的重复元素(仅保留一个) |
sort() |
对链表元素进行排序(默认升序) |
merge(list& x) |
合并两个已排序的链表,结果放入当前链表,x 变为空 |
cpp
// resize
list<int> lst = {1, 2, 3};
lst.resize(5, 100); // {1, 2, 3, 100, 100}
lst.resize(2); // {1, 2}
// assign
lst.assign(4, 7); // {7, 7, 7, 7}
list<int> other = {10, 20, 30};
lst.assign(other.begin(), other.end()); // {10, 20, 30}
// swap
list<int> a = {1, 2, 3};
list<int> b = {4, 5, 6};
a.swap(b); // a = {4,5,6}, b = {1,2,3}
// reverse
list<int> nums = {1, 2, 3, 4, 5};
nums.reverse(); // {5, 4, 3, 2, 1}
// unique(仅删除相邻重复)
list<int> dup = {1, 1, 2, 2, 2, 3, 1, 1};
dup.unique(); // {1, 2, 3, 1} ------ 注意最后两个1不相邻,未被删除
// sort
list<int> unsorted = {5, 2, 8, 1, 9};
unsorted.sort(); // {1, 2, 5, 8, 9}
unsorted.sort(greater<int>()); // 降序 {9, 8, 5, 2, 1}
// merge(需两个list都已排序)
list<int> l1 = {1, 3, 5};
list<int> l2 = {2, 4, 6};
l1.merge(l2); // l1 = {1,2,3,4,5,6}, l2 = {}
二、list迭代器失效问题
1. list的底层实现特点
std::list底层是双向链表,每个节点独立分配内存,节点间通过指针连接。这种结构决定了:
-
插入/删除操作只影响相邻节点的指针,不会导致其他节点内存移动
-
迭代器本质上是指向链表节点的指针
2. 迭代器失效场景分析
2.1 插入操作:不失效 ✅
cpp
#include <list>
#include <iostream>
int main() {
std::list<int> lst = {1, 2, 3};
auto it = lst.begin(); // 指向元素1
lst.insert(it, 100); // 在it之前插入100
std::cout << *it << std::endl; // 依然输出1,it未失效
// 输出:1
}
结论 :list的插入操作不会导致任何已有迭代器失效。
2.2 删除操作:被删元素的迭代器失效 ❌
cpp
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin(); // it指向1
++it; // it指向2
lst.erase(it); // 删除元素2
// std::cout << *it << std::endl; // 未定义行为!it已失效
关键点:只有被删除元素对应的迭代器失效,其他迭代器(包括指向前后元素的迭代器)依然有效。
2.3 正确删除的两种方式
方式一:利用erase返回值
cpp
std::list<int> lst = {1, 2, 3, 4, 5};
for (auto it = lst.begin(); it != lst.end(); ) {
if (*it % 2 == 0) {
it = lst.erase(it); // erase返回下一个元素的有效迭代器
} else {
++it;
}
}
// lst = {1, 3, 5}
方式二:使用remove/remove_if(更简洁)
cpp
std::list<int> lst = {1, 2, 3, 4, 5};
lst.remove_if([](int n) { return n % 2 == 0; });
// lst = {1, 3, 5}
2.4 clear()操作:所有迭代器失效 ❌
cpp
std::list<int> lst = {1, 2, 3};
auto it = lst.begin();
lst.clear(); // 清空所有元素
// it 已失效,不能再使用
2.5 splice操作:迭代器保持有效 ✅
splice(转移节点)是list特有的操作,转移的节点本身不会导致迭代器失效:
cpp
std::list<int> lst1 = {1, 2, 3};
std::list<int> lst2 = {4, 5, 6};
auto it = lst1.begin(); // 指向1
auto it2 = lst2.begin(); // 指向4
lst1.splice(it, lst2, it2); // 将lst2的it2节点转移到lst1中it之前
std::cout << *it2 << std::endl; // 输出4,it2依然有效(指向被转移的节点)
// 但注意:it2现在属于lst1了
3. 与其他容器的对比
| 操作 | vector | list | deque |
|---|---|---|---|
| 头部插入 | 所有迭代器失效 | 不失效 | 可能失效 |
| 中间插入 | 所有迭代器失效 | 不失效 | 可能失效 |
| 尾部插入 | 仅end失效(扩容时全部失效) | 不失效 | 不失效 |
| 删除元素 | 被删元素及之后迭代器失效 | 仅被删元素失效 | 被删元素及之后迭代器失效 |
4. 常见陷阱与最佳实践
陷阱1:循环中错误删除
cpp
// ❌ 错误写法
for (auto it = lst.begin(); it != lst.end(); ++it) {
if (*it == target) {
lst.erase(it); // it失效后还执行++it,未定义行为
}
}
// ✅ 正确写法
for (auto it = lst.begin(); it != lst.end(); ) {
if (*it == target) {
it = lst.erase(it);
} else {
++it;
}
}
陷阱2:保存迭代器后执行删除
cpp
std::list<int> lst = {1, 2, 3};
auto it = std::next(lst.begin()); // 指向2
lst.erase(lst.begin()); // 删除1,it依然有效 ✅
lst.erase(it); // 删除2,有效
// 但注意:如果删除的就是it指向的元素
auto it2 = lst.begin();
lst.erase(it2); // it2失效
// std::cout << *it2; // 错误!
陷阱3:list的迭代器与vector不同
cpp
// vector的insert会导致迭代器失效
std::vector<int> vec = {1, 2, 3};
auto vit = vec.begin();
vec.insert(vit, 100);
// std::cout << *vit; // ❌ 未定义行为,vit可能失效
// list的insert安全得多
std::list<int> lst = {1, 2, 3};
auto lit = lst.begin();
lst.insert(lit, 100);
std::cout << *lit; // ✅ 输出1,安全
三、list与vector对比
在 C++ STL 中,vector 和 list 是最常用的两种序列式容器。虽然它们都可以存储元素,但底层实现差异巨大,直接决定了它们的性能表现与适用场景。
1. 底层结构
| 特性 | vector | list |
|---|---|---|
| 底层结构 | 动态顺序表,一段连续内存空间 | 带头结点的双向循环链表 |
| 内存分配 | 一次性分配连续内存,容量不足时重新分配并拷贝 | 每次插入动态分配节点内存 |
2. 访问方式
-
vector :支持随机访问(下标
[]),时间复杂度 O(1),适合频繁读取任意位置元素。 -
list :不支持随机访问,只能通过迭代器顺序遍历,访问中间元素时间复杂度 O(N)。
3. 插入与删除
-
vector:
-
非尾部插入/删除需要移动大量元素,时间复杂度 O(N)
-
尾部插入可能触发扩容(重新分配、拷贝、释放旧空间),开销较大
-
-
list:
-
任意位置插入/删除只需调整指针,时间复杂度 O(1)
-
无扩容概念,不会引起整体元素拷贝
-
4. 空间与缓存效率
-
vector:
-
空间连续,缓存友好,遍历效率高
-
内存碎片少,空间利用率高
-
-
list:
-
节点分散,缓存不友好,遍历时频繁跳转
-
小节点易造成内存碎片,额外存储前后指针(通常 8~16 字节/节点)
-
5. 迭代器
| 特性 | vector | list |
|---|---|---|
| 迭代器类型 | 原生指针 | 对节点指针的封装 |
| 插入时失效 | 可能全部失效(扩容导致) | 不会失效 |
| 删除时失效 | 删除位置及之后的迭代器失效 | 仅当前迭代器失效 |
⚠️ 迭代器失效是实际开发中极易踩的坑,尤其是在遍历过程中做删除或插入操作,必须谨慎处理。
6. 适用场景
| 场景 | 推荐容器 | 理由 |
|---|---|---|
| 需要频繁随机访问、尾部增删 | vector | O(1) 随机访问,尾部增删高效 |
| 大量中间插入/删除,很少访问 | list | O(1) 插入/删除,无扩容成本 |
| 小数据量、频繁遍历 | vector | 缓存命中率高 |
| 需要稳定迭代器(插入不失效) | list | 迭代器稳定性强 |