在 C++ STL 中,list 是一个基于双向循环链表实现的序列容器。与 vector 的连续内存布局不同,list 通过节点指针将元素链接在一起,这种结构决定了它独特的性能特征。理解 list 的底层实现是掌握其用法的关键。
list 的每个节点包含三个部分:
特别值得注意的是,标准库实现的 list 采用了一个巧妙的设计——哨兵位头结点。这个特殊节点不存储有效数据,它的存在使得代码逻辑更加统一:
cpp复制struct __list_node {
__list_node* prev;
__list_node* next;
T data;
};
哨兵节点的作用体现在:
list 的核心操作时间复杂度如下表所示:
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 插入/删除 | O(1) | 只需调整指针指向 |
| 随机访问 | O(n) | 需要从头遍历 |
| 排序 | O(n log n) | 通常使用归并排序实现 |
| 查找 | O(n) | 线性搜索 |
| 合并 | O(1) | 只需修改头尾指针(若已排序) |
这种性能特征使得 list 特别适合频繁插入删除但很少随机访问的场景。
list 提供了多种构造方式,满足不同初始化需求:
cpp复制// 默认构造(空链表)
list<int> lst1;
// 填充构造(5个值为42的元素)
list<int> lst2(5, 42);
// 范围构造(使用迭代器范围)
int arr[] = {1, 3, 5, 7, 9};
list<int> lst3(arr, arr + sizeof(arr)/sizeof(arr[0]));
// 拷贝构造
list<int> lst4(lst3);
// C++11 初始化列表构造
list<int> lst5 = {2, 4, 6, 8, 10};
实际工程中,范围构造特别有用,它允许我们从各种数据源(数组、vector等)初始化 list。需要注意的是,当初始化大量元素时,emplace 系列方法通常比先构造再插入更高效。
list 的迭代器属于双向迭代器类别,支持以下操作:
但不同于 vector 的随机访问迭代器,list 迭代器不支持:
典型遍历方式示例:
cpp复制// 正向遍历
for (auto it = lst.begin(); it != lst.end(); ++it) {
cout << *it << " ";
}
// 反向遍历
for (auto rit = lst.rbegin(); rit != lst.rend(); ++rit) {
cout << *rit << " ";
}
// C++11 范围for(推荐)
for (const auto& val : lst) {
cout << val << " ";
}
重要提示:list 的 end() 迭代器指向的是哨兵节点,而不是最后一个元素。这是很多初学者容易混淆的地方。
虽然 list 不支持随机访问,但它提供了特定的访问接口:
cpp复制list<int> lst = {10, 20, 30, 40};
// 访问首尾元素(引用方式,可修改)
lst.front() = 100; // 修改第一个元素
lst.back() = 400; // 修改最后一个元素
// 安全的元素访问模式
if (!lst.empty()) {
cout << "First element: " << lst.front() << endl;
cout << "Last element: " << lst.back() << endl;
}
修改操作是 list 的强项,所有插入删除操作都保证 O(1) 时间复杂度:
cpp复制// 头部操作
lst.push_front(5); // 头部插入
lst.pop_front(); // 头部删除
// 尾部操作
lst.push_back(50); // 尾部插入
lst.pop_back(); // 尾部删除
// 任意位置操作
auto it = lst.begin();
advance(it, 2); // 移动到第三个位置
lst.insert(it, 25); // 在第三个位置前插入
it = lst.erase(it); // 删除当前元素,返回下一个位置的迭代器
性能提示:虽然 insert/erase 是 O(1) 操作,但找到插入位置可能需要 O(n) 时间。如果需要频繁在特定位置操作,考虑使用其他数据结构。
由于 list 的特殊迭代器性质,它提供了自己的 sort() 成员函数:
cpp复制list<int> lst = {5, 3, 1, 4, 2};
// 升序排序(默认)
lst.sort();
// 降序排序
lst.sort(greater<int>());
// 自定义排序(如按绝对值)
lst.sort([](int a, int b) {
return abs(a) < abs(b);
});
去重操作通常需要先排序:
cpp复制list<int> lst = {1, 2, 2, 3, 3, 3, 4};
lst.unique(); // 仅移除连续重复
// 结果:1, 2, 3, 4
// 完全去重(先排序)
lst.sort();
lst.unique();
list 的 merge 操作要求两个链表都已排序:
cpp复制list<int> lst1 = {1, 3, 5};
list<int> lst2 = {2, 4, 6};
lst1.merge(lst2);
// lst1: 1, 2, 3, 4, 5, 6
// lst2: 空
拼接(splice)操作可以在不排序的情况下移动元素:
cpp复制list<int> lst1 = {1, 2, 3};
list<int> lst2 = {4, 5, 6};
// 将lst2的全部元素移动到lst1末尾
lst1.splice(lst1.end(), lst2);
// 选择性移动单个元素
auto it = lst1.begin();
advance(it, 2); // 指向第三个元素
lst2.splice(lst2.begin(), lst1, it); // 移动单个元素
对于性能敏感的场景,list 提供了内存控制接口:
cpp复制list<int> lst;
// 预分配节点(减少动态分配开销)
lst.reserve(100); // 注意:这是非标准扩展,部分实现可能不支持
// 释放未用内存(C++11)
lst.shrink_to_fit();
list 的迭代器失效规则相对简单:
cpp复制list<int> lst = {1, 2, 3, 4, 5};
auto it1 = lst.begin(); // 指向1
auto it2 = next(it1, 2); // 指向3
lst.erase(it1); // it1失效,it2仍然有效
// 正确做法:获取erase返回的新迭代器
it2 = lst.erase(it2); // it2现在指向4
推荐的安全使用模式:
cpp复制for (auto it = lst.begin(); it != lst.end(); /* 无自增 */) {
if (should_remove(*it)) {
it = lst.erase(it); // 获取新迭代器
} else {
++it;
}
}
对于多线程环境,需要额外的同步机制:
cpp复制mutex mtx;
list<int> shared_lst;
// 线程安全操作
{
lock_guard<mutex> lock(mtx);
auto it = find(shared_lst.begin(), shared_lst.end(), value);
if (it != shared_lst.end()) {
it = shared_lst.erase(it);
}
}
通过基准测试可以直观看到两者的差异(单位:纳秒):
| 操作 | list (100k) | vector (100k) |
|---|---|---|
| 头部插入 | 5 | 500,000 |
| 中间插入 | 10 | 250,000 |
| 尾部插入 | 5 | 5 |
| 随机访问 | 50,000 | 5 |
| 删除头部 | 5 | 500,000 |
| 删除中间 | 10 | 250,000 |
根据需求选择容器的决策流程:
是否需要频繁随机访问?
插入/删除主要在什么位置?
元素是否为大型对象?
是否需要内存连续性?
在实际工程中,常常结合两者优势:
cpp复制// 场景:频繁修改但偶尔需要随机访问
list<Item> active_items;
vector<list<Item>::iterator> index;
// 添加元素
active_items.push_back(item);
index.push_back(--active_items.end());
// 随机访问(通过索引)
Item& third_item = *index[2];
list 的节点通常是单独分配的,这可能导致内存碎片。优化策略包括:
cpp复制template <typename T>
class PoolAllocator {
// 实现内存池分配
};
list<int, PoolAllocator<int>> optimized_list;
cpp复制list<int> lst;
lst.reserve(1000); // 非标准,但部分实现支持
虽然 list 本身缓存不友好,但可以通过以下方式改善:
cpp复制struct Group {
array<int, 16> data; // 一组16个元素
// 其他元数据
};
list<Group> grouped_list;
cpp复制array<list<int>, 100> small_lists; // 100个小list
list 本身不是线程安全的,多线程环境下需要:
cpp复制class ThreadSafeList {
list<int> data;
mutable mutex mtx;
public:
void insert(int value) {
lock_guard<mutex> lock(mtx);
data.push_back(value);
}
// 其他线程安全接口
};
cpp复制atomic<list<int>::iterator> atomic_head;
// 需要精心设计无锁算法
cpp复制auto it = lst.begin();
lst.erase(it);
cout << *it << endl; // 未定义行为!
cpp复制// 错误:O(n)时间复杂度
for (size_t i = 0; i < lst.size(); ++i) {
/* ... */
}
// 正确:保存size
size_t s = lst.size();
for (size_t i = 0; i < s; ++i) {
/* ... */
}
bash复制# 打印list内容
p *(lst._M_impl._M_node._M_next)@lst.size()
bash复制valgrind --tool=memcheck ./your_program
bash复制perf stat -e cache-misses ./your_program
cpp复制template <typename K, typename V>
class LRUCache {
list<pair<K, V>> items;
unordered_map<K, typename list<pair<K, V>>::iterator> index;
size_t capacity;
public:
LRUCache(size_t cap) : capacity(cap) {}
V* get(const K& key) {
auto it = index.find(key);
if (it == index.end()) return nullptr;
items.splice(items.begin(), items, it->second);
return &it->second->second;
}
void put(const K& key, const V& value) {
auto it = index.find(key);
if (it != index.end()) {
items.erase(it->second);
}
items.emplace_front(key, value);
index[key] = items.begin();
if (index.size() > capacity) {
index.erase(items.back().first);
items.pop_back();
}
}
};
cpp复制class TaskScheduler {
array<list<Task>, 5> priority_queues;
mutex queue_mutex;
condition_variable cv;
public:
void add_task(Task&& task, int priority) {
lock_guard<mutex> lock(queue_mutex);
priority_queues[priority].push_back(move(task));
cv.notify_one();
}
Task get_task() {
unique_lock<mutex> lock(queue_mutex);
cv.wait(lock, [this]{
return any_of(priority_queues.begin(), priority_queues.end(),
[](auto& q){ return !q.empty(); });
});
for (auto& queue : priority_queues) {
if (!queue.empty()) {
Task task = move(queue.front());
queue.pop_front();
return task;
}
}
throw logic_error("No task available");
}
};
cpp复制list<int> lst = {1, 2, 3, 4, 5};
// 过滤偶数并转换平方
auto view = lst | views::filter([](int x){ return x % 2 == 0; })
| views::transform([](int x){ return x * x; });
for (int x : view) {
cout << x << " "; // 输出:4 16
}
cpp复制generator<int> traverse(list<int>& lst) {
for (int x : lst) {
co_yield x;
}
}
list<int> lst = {1, 2, 3};
for (int x : traverse(lst)) {
cout << x << " ";
}
在实际工程中,list 是一个强大但常被误解的容器。理解其底层原理和性能特征,才能做出最合适的设计选择。当需要频繁在序列中间插入删除时,list 通常是比 vector 更好的选择,但要注意其缓存不友好的特点。