C++ 中 std::list使用详解和实战示例
1. 定义与特性
-
std::list
是 双向链表(doubly linked list)的封装。 -
所有元素在内存中不是连续存储的,每个节点保存元素值及前后指针。
-
特点:
- 插入、删除 任意位置元素 的时间复杂度是 O(1)(前提是已知位置的迭代器)。
- 不支持随机访问(没有
operator[]
)。 - 迭代器是 双向迭代器(bidirectional iterator),不能做
it + n
这样的操作。
2. 头文件与定义
#include <list>
#include <iostream>
using namespace std;int main() {list<int> lst; // 空链表list<int> lst2(5, 10); // 含 5 个元素,值为 10list<int> lst3 = {1,2,3,4}; // 列表初始化
}
3. 迭代器类型
begin()
/end()
:正向迭代器rbegin()
/rend()
:反向迭代器cbegin()
/cend()
:常量迭代器
list<int> lst = {1, 2, 3};
for (auto it = lst.begin(); it != lst.end(); ++it) {cout << *it << " ";
}
4. 常用接口
构造与赋值
list<int> a; // 空
list<int> b(5, 1); // 5 个 1
list<int> c(b); // 拷贝
list<int> d = {1,2,3,4}; // 初始化列表
a = d; // 赋值
插入与删除
list<int> lst = {1, 2, 3};// 头尾操作
lst.push_back(4); // 尾部插入
lst.push_front(0); // 头部插入
lst.pop_back(); // 尾部删除
lst.pop_front(); // 头部删除// 任意位置
auto it = lst.begin();
++it; // 指向第二个元素
lst.insert(it, 99); // 在 it 前插入 99
lst.erase(it); // 删除 it 指向的元素lst.clear(); // 清空
容量
lst.size(); // 元素个数
lst.empty(); // 是否为空
lst.max_size(); // 理论最大容量
访问
std::list
不支持随机访问,没有operator[]
。只能通过迭代器或front()
/back()
。
cout << lst.front(); // 第一个元素
cout << lst.back(); // 最后一个元素
5. std::list
独有接口
这些操作利用链表特性,效率很高:
list<int> a = {1, 3, 5};
list<int> b = {2, 4, 6};a.merge(b); // 归并 b 到 a(要求已排序)
a.splice(a.begin(), b); // 把 b 的元素整体移动到 a 的开头
a.remove(3); // 删除所有值为 3 的元素
a.remove_if([](int x){ return x % 2 == 0; }); // 删除偶数
a.reverse(); // 反转链表
a.unique(); // 删除相邻重复元素(要求已排序)
a.sort(); // 内部排序(链表版归并排序)
6. 时间复杂度总结
- 插入/删除(已知迭代器位置):O(1)
- 查找第 k 个元素:O(n)(只能顺序走迭代器)
- 访问头尾元素:O(1)
- 排序:O(n log n)(链表版归并排序)
- 遍历:O(n)
7. 与 vector
、deque
对比
特性 | vector | deque | list |
---|---|---|---|
底层结构 | 连续数组 | 分段连续数组 | 双向链表 |
随机访问 | O(1) | O(1) | 不支持 |
插入删除效率 | 尾部 O(1),中间 O(n) | 头尾 O(1),中间 O(n) | 任意位置 O(1)(已知迭代器) |
内存局部性 | 好 | 一般 | 差 |
排序方式 | std::sort (快排/堆排) | std::sort | 内置 list::sort() (归并排序) |
总结:
- 如果你需要 频繁在任意位置插入/删除 → 用
list
。 - 如果需要 高效随机访问 → 用
vector
。 - 如果需要 两端操作 + 随机访问 → 用
deque
。
8. 使用注意事项
-
迭代器稳定性:
- 对
list
来说,插入、删除不会使其他元素的迭代器失效(只会失效指向被删除的元素)。 - 这是区别于
vector
的大优势。
- 对
-
内存开销:
- 每个节点都需要额外存储两个指针,开销比
vector
大。
- 每个节点都需要额外存储两个指针,开销比
-
遍历性能:
- 因为不连续,CPU cache 利用率差,遍历速度一般比
vector
慢很多。
- 因为不连续,CPU cache 利用率差,遍历速度一般比
9、std::list
节点插入 / 删除过程演示
1. 节点结构
在 std::list
中,每个元素存储在一个节点里,节点是 双向链表:
+-------------------+
| value (数据) |
| prev 指针 | ---> 指向前一个节点
| next 指针 | ---> 指向后一个节点
+-------------------+
所有节点通过 prev
和 next
串起来,形成双向链表。
通常 STL 的实现会有一个 哨兵节点 (sentinel / dummy node),让 begin()
/ end()
更统一。
2. 初始链表
假设我们有一个 std::list<int>
,存了 [10, 20, 30]
:
head → [10] ⇆ [20] ⇆ [30] ← tail
更细致的图(⇆
表示双向连接):
nullptr ← [10] ⇆ [20] ⇆ [30] → nullptr
3. 插入节点
我们想在 20
前插入一个新元素 15
。
插入前:
[10] ⇆ [20] ⇆ [30]
插入过程:
-
分配一个新节点
15
。 -
改变指针关系:
15.prev = 10
15.next = 20
10.next = 15
20.prev = 15
插入后:
[10] ⇆ [15] ⇆ [20] ⇆ [30]
注意:只改动 常数个指针,所以插入复杂度 O(1)。
而 vector
插入时可能需要 移动大量元素。
4. 删除节点
我们删除元素 20
。
删除前:
[10] ⇆ [15] ⇆ [20] ⇆ [30]
删除过程:
-
改变指针关系:
15.next = 30
30.prev = 15
-
释放
20
节点内存。
删除后:
[10] ⇆ [15] ⇆ [30]
删除同样只修改 常数个指针,复杂度 O(1)。
5. 内存对比:vector
vs list
vector
连续存储:
[10][15][20][30][40]...
- 插入 / 删除中间元素 → 需要 搬移后续元素。
- 优点:cache 友好,随机访问 O(1)。
list
分散存储:
[10] ⇆ [15] ⇆ [20] ⇆ [30]
- 插入 / 删除只改指针。
- 缺点:不支持随机访问,遍历性能差于
vector
。
6. 图示总结
- 插入节点时:只需改 4 条指针。
- 删除节点时:只需改 2 条指针 + 释放内存。
- 这就是
list
在频繁插入 / 删除时比vector
高效的原因。
10. 示例程序
#include <iostream>
#include <list>
using namespace std;int main() {list<int> lst = {5, 2, 3, 2, 4};cout << "初始链表: ";for (int x : lst) cout << x << " ";cout << endl;lst.push_front(1);lst.push_back(6);cout << "插入后: ";for (int x : lst) cout << x << " ";cout << endl;lst.remove(2); // 删除所有值为 2cout << "删除值 2 后: ";for (int x : lst) cout << x << " ";cout << endl;lst.sort();cout << "排序后: ";for (int x : lst) cout << x << " ";cout << endl;lst.reverse();cout << "反转后: ";for (int x : lst) cout << x << " ";cout << endl;
}
11. 实战应用示例-任务调度队列
背景
在 SLAM 或点云处理系统中,我们经常要维护一个 任务队列:
- 每个任务有不同的优先级(高优先任务要尽快处理)。
- 任务可能在运行时随时被取消 / 插入。
- 任务完成后要从队列中移除。
这种情况下:
- 随机访问需求很少(不需要按索引访问第 k 个任务)。
- 频繁插入 / 删除(尤其是中间位置)。
这就是std::list
的优势场景。
示例代码
#include <iostream>
#include <list>
#include <string>struct Task {int id;int priority; // 数字越大,优先级越高std::string name;
};// 打印任务队列
void printTasks(const std::list<Task>& tasks) {for (auto& t : tasks) {std::cout << "[ID=" << t.id << ", P=" << t.priority << ", " << t.name << "] ";}std::cout << "\n";
}int main() {std::list<Task> taskQueue;// 插入一些任务(模拟实时到来)taskQueue.push_back({1, 1, "Mapping"});taskQueue.push_back({2, 3, "LoopClosure"});taskQueue.push_back({3, 2, "Optimization"});std::cout << "初始任务队列:\n";printTasks(taskQueue);// ===== 根据优先级插入新任务 =====Task newTask = {4, 5, "HighPriorityLocalization"};auto it = taskQueue.begin();// 找到插入位置(保持优先级降序)while (it != taskQueue.end() && it->priority >= newTask.priority) {++it;}taskQueue.insert(it, newTask);std::cout << "\n插入高优先级任务后:\n";printTasks(taskQueue);// ===== 删除任务(任务完成) =====for (auto it = taskQueue.begin(); it != taskQueue.end(); ) {if (it->id == 2) { // 删除 LoopClosure 任务it = taskQueue.erase(it);} else {++it;}}std::cout << "\n删除任务 ID=2 后:\n";printTasks(taskQueue);// ===== 动态调度(修改任务优先级 -> 移动位置) =====for (auto it = taskQueue.begin(); it != taskQueue.end(); ) {if (it->id == 1) {Task updated = *it;updated.priority = 6; // 提升优先级it = taskQueue.erase(it); // 删除旧位置// 重新插入合适位置auto pos = taskQueue.begin();while (pos != taskQueue.end() && pos->priority >= updated.priority) {++pos;}taskQueue.insert(pos, updated);} else {++it;}}std::cout << "\n任务 ID=1 提升优先级后:\n";printTasks(taskQueue);return 0;
}
输出示例
初始任务队列:
[ID=1, P=1, Mapping] [ID=2, P=3, LoopClosure] [ID=3, P=2, Optimization] 插入高优先级任务后:
[ID=4, P=5, HighPriorityLocalization] [ID=2, P=3, LoopClosure] [ID=3, P=2, Optimization] [ID=1, P=1, Mapping] 删除任务 ID=2 后:
[ID=4, P=5, HighPriorityLocalization] [ID=3, P=2, Optimization] [ID=1, P=1, Mapping] 任务 ID=1 提升优先级后:
[ID=1, P=6, Mapping] [ID=4, P=5, HighPriorityLocalization] [ID=3, P=2, Optimization]
为什么用 std::list
-
插入任务 → 只需找到位置然后 O(1) 修改指针。
-
删除任务 → 直接用迭代器
erase()
,O(1)。 -
修改优先级 → 先
erase()
,再insert()
,同样 O(1)。 -
如果换成
vector
:- 插入 / 删除会导致元素移动,最坏 O(n)。
- 任务数量大时会严重影响实时性。
真实项目中的应用场景
- SLAM 后端优化任务调度:前端关键帧插入 → 优先放高优先任务(回环检测)。
- 点云处理流水线:需要随时插入 / 取消滤波任务。
- 网络/分布式系统:消息队列(部分场景
std::deque
也可)。 - 游戏引擎:实体的事件调度(插入/删除频繁)。