往期内容回顾
C++ std::list全面指南:从连续容器到节点容器
前言
在前面的学习中,我们已经熟悉了:
-
std::string:本质是“动态字符数组”,连续存储。
-
std::vector:通用的“连续动态数组”,随机访问 O(1)、扩容几何增长。
但在需要频繁在中间插入/删除、或保持指针/引用稳定时,连续存储并不友好。
这就是 std::list(双向链表) 的用武之地:它用节点(node)存放元素,每个节点维护前驱/后继指针,通过指针重连完成 O(1) 的插入/删除,并且在大多数修改下迭代器、指针、引用保持有效。
一句话对比:vector 追求 局部性与随机访问;list 追求 插删稳定性与指针/迭代器稳定。
主要内容预览
本文将分节介绍:
1、std::list 的数据结构与特性
2、与 vector/string 的核心对比
3、常用接口与示例(构造、遍历、插删、特殊算法)
4、迭代器与失效规则
5、复杂度与性能、何时使用/避免
6、进阶:splice/merge/sort/unique/remove 的“节点级”威力
7、常见坑与最佳实践
8、小结与选型建议
一、数据结构与核心特性
1.1 节点式存储
std::list<T> 是双向链表。每个节点包含:
-
一个 T 对象(元素本体)
-
两个指针:prev / next(前驱/后继)
后果:
-
内存不连续,节点分散在堆上。
-
没有随机访问(不支持 operator[]、at),只能顺着迭代器走。
-
在任意位置 插入/删除 元素都是 O(1)(已知位置)。
1.2 迭代器类别
-
std::list 的迭代器是 双向迭代器(BidirectionalIterator)。
-
只能 ++it / --it,不能 it + n。
-
与 vector 的 随机访问迭代器 不同,所以很多需要随机访问的算法(如 std::sort)不能直接用于 list。
1.3 size() 复杂度
-
现代实现通常是 O(1)。
-
历史上(老旧实现)可能是 O(n)。
-
std::forward_list 的 size() 仍可能是 O(n)。
二、与 vector / string的核心对比
维度 | string / vector(连续) | list(节点) |
---|---|---|
内存布局 | 连续 | 分散(每元素一个节点) |
随机访问 | O(1) | 不支持 |
中间插入/删除 | O(n)(需要挪动大段元素) | O(1)(指针重连) |
push_back | 均摊 O(1),扩容时重分配 | O(1) |
迭代器稳定性 | 扩容或中间插删会大量失效 | 除被删元素外通常都有效 |
缓存友好 | 非常好(局部性强) | 较差(指针追踪) |
内存开销 | 小(头部管理 + 连续块) | 大(每节点两指针 + 分配器开销) |
结论:
-
有大量随机访问、遍历占主:优先 vector。
-
需要频繁在中间插删、需要稳定迭代器/引用:考虑 list。
-
如果只是首尾插删且仍希望较好的随机访问:考虑 deque。
三、快速上手:常用接口与示例
1、list的构造
int main() { list<int> L1; // 空 list<int> L2(5, 42); // 5 个 42 list<int> L3 = {1, 2, 3}; // 列表初始化 list<int> L4(L3); // 拷贝构造 list<int> L5(::move(L3)); // 移动构造(L3 被掏空) cout << L2.size() << endl; // 通常 O(1) cout << boolalpha << L1.empty() << endl; for(auto e:L3){ cout << e <<" " ; } cout << endl; for(auto e:L4){ cout << e <<" " ; } cout << endl; for(auto e:L5){ cout << e <<" " ; } cout << endl; return 0; }
输出描述:
5
true1 2 3
1 2 3
2 遍历(不支持随机访问)
list不支持运算符[ ];
void Print_list(const list<int>& l1){ list<int>::const_iterator it = l1.begin(); while(it != l1.end()){ cout<< *it <<" "; ++it; } cout<<endl; } int main(){ list<int> l1; l1.push_back(1); l1.push_back(2); l1.push_back(3); l1.push_back(4); for(auto e: l1){ cout << e <<" " ; } for (auto rit = L5.rbegin(); rit != L5.rend(); ++rit) cout << *rit << ' '; cout<<endl; Print_list(l1);
输出描述:
1 2 3 4
4 3 2 1
1 2 3 4
3、 首尾与任意位置插删
void test2(){ list<int> L = {1, 3, 5}; L.push_front(0); // 0 1 3 5 L.push_back(7); // 0 1 3 5 7 L.pop_front(); // 1 3 5 7 L.pop_back(); // 1 3 5 Print_list(L); } int main(){ test2(); }
与 vector 的重要差异: 这些插删操作不会移动其他元素对象本体,因此 指向其他元素的指针/引用/迭代器仍然有效(被删元素除外)。输出描述:1 3 5
insert() —— 在任意位置插入
作用:在迭代器 pos 指向的位置前面插入元素。
时间复杂度:O(1)(已知迭代器位置的情况下);注意链表的空间不连续,迭代器不支持加法。
常用重载:
iterator insert(iterator pos, const T& value); // 插入一个值
iterator insert(iterator pos, size_type count, const T& val);// 插入 count 个相同值
template<class InputIt>
iterator insert(iterator pos, InputIt first, InputIt last); // 插入范围 [first, last)
iterator insert(iterator pos, T&& value); // 插入右值
代码实现:
void test3(){ list<int> L = {1, 3, 5}; list<int>::iterator it = L.begin(); it++; L.insert(it,2); // 0 1 3 5 Print_list(L); L.insert(L.end(),3,6); Print_list(L); } int main(){ test3(); }
输出描述:
1 2 3 5
1 2 3 5 6 6 6template<class Inputiterator, class T> Inputiterator find(Inputiterator first, Inputiterator last,const T& val){ while(first != last){ if(*first == val){ return first; } ++first; } return last; } void test4(){ list<int> L = {1,2,3,4,5,6}; list<int>::iterator it = find(L.begin(),L.end(),3); L.insert(it,8); // 0 1 3 5 if(it!=L.end()){ Print_list(L); } // L.insert(L.end(),3,6); }
输出描述:
1 2 8 3 4 5 6
注意:迭代器与失效规则(与 vector 强烈对比)
-
插入/删除:
-
删除:仅被删元素的迭代器/引用失效;其他全部保持有效。
-
插入:不使现有元素的迭代器/引用失效。
-
-
对比 vector: 扩容或中间插删会导致大批迭代器失效,指针/引用也可能失效。
测试:
void test6(){ list<int> L = {1,2,3,4,5,6}; list<int>::iterator pos = find(L.begin(),L.end(),3); pos = L.erase(pos); L.insert(pos,3); Print_list(L); }
输出描述:
zsh: segmentation fault "/Users/junye/Desktop/cplusplus/"List
正确做法:用 erase 返回值
void test6(){ list<int> L = {1,2,3,4,5,6}; list<int>::iterator pos = find(L.begin(),L.end(),3); pos = L.erase(pos); L.insert(pos,3); Print_list(L); }
输出结果:
1 2 3 4 5 6
4. emplace() / emplace_front() / emplace_back() —— 原地构造
作用:在指定位置直接构造对象,避免不必要的复制或移动。
时间复杂度:O(1)
示例
struct Person { string name; int age; Person(string n, int a) : name(n), age(a) {} }; list<Person> people; people.emplace_back("Alice", 25); // 尾部直接构造 people.emplace_front("Bob", 30); // 头部直接构造 auto itp = people.begin(); ++itp; people.emplace(itp, "Charlie", 28); // 任意位置构造
特点:
-
和 insert() 相比,emplace() 省略了临时对象的构造+拷贝/移动过程。
-
推荐在构造复杂对象时使用。
5. splice() —— 从另一个 list移动节点
作用:将另一个 list 的节点直接接入当前 list,不复制数据,而是直接修改指针。
时间复杂度:O(1)(节点链接修改)
示例:
list<int> l1 = {1, 2, 3}; list<int> l2 = {10, 20, 30}; auto it = l1.begin(); ++it; // 指向 2 l1.splice(it, l2); // 将 l2 所有元素插入到 l1 的 2 前面
特点:
节省内存拷贝,适合大数据结构的快速转移。
目标和源必须是同类型 list。
举个例子:
void test7(){ list<int> L1 = {1,2,3,4,5,6}; list<int> L2 = {1,2,3,4,5,6}; L1.splice(L1.end(),L2); Print_list(L1); Print_list(L2); } void test8(){ list<int> L1 = {1,2,3,4,5,6}; list<int> L2 = {1,2,3,4,5,6}; L1.splice(L1.end(),L2,++L2.begin(),--L2.end()); Print_list(L1); Print_list(L2); } int main(){ test7(); test8(); }
输出描述:
1 2 3 4 5 6 1 2 3 4 5 6
1 2 3 4 5 6 2 3 4 5
1 6
6、其他常用的一些操作
merge:线性合并两个有序 list(稳定)
list<int> A = {1,3,5}; list<int> B = {2,4,6}; A.merge(B); // A: 1 2 3 4 5 6, B 空
sort:链表专用排序(稳定排序)
list<string> L = {"bbb","a","cc"}; L.sort(); // a, bbb, cc L.sort([](auto& x, auto& y){ return x.size() < y.size(); });
unique:移除相邻重复元素
list<int> L = {1,1,2,3,3,3,4}; L.unique(); // 1 2 3 4(只去相邻重复) L.unique([](int a, int b){ return abs(a-b) <= 1; });
remove / remove_if:按值(或谓词)删除元素
list<int> L = {1,2,3,2,4}; L.remove(2); // 删除所有等于 2 的元素 L.remove_if([](int x){ return x%2==0; }); // 删除所有偶数
splice/merge/sort/unique:
节点只是“换链接”或“重排顺序”,元素本体地址不变,因此迭代器、引用仍有效(但可能属于另一个容器,或顺序发生变化)
总结
-
std::list 是典型的节点式容器:
-
优势:已知位置插删 O(1)、迭代器/引用稳定、强大的节点级算法(splice/merge/sort/unique/remove)。
-
劣势:不支持随机访问、缓存局部性差、内存开销大,纯遍历吞吐通常不及 vector。
-
-
选型心法:
-
读多写少、随机访问 → vector;
-
频繁在中间插删 & 需要稳定迭代器 → list;
-
双端频繁操作且需随机访问 → deque。
-
-
真正发挥 list 价值的,是节点级别的 O(1) 操作(特别是 splice/merge),这在 vector/string 的连续模型里是做不到的。
学会在“连续容器思维”(vector/string)与“节点容器思维”(list)之间切换,
你就能根据需求做出更贴近底层的、性能与可维护性兼顾的容器选型