C++中的list(1)
同理,本期只讲使用,不深究。
先整体的介绍一遍list
你可以把 std::list
想象成一个 双向链表 。想象一下手拉手(或者更确切地说,是手拉手 并且 背靠背)站成一排的人:
[人A] <--> [人B] <--> [人C] <--> ... <--> [人Z]
- 每个人(节点): 代表链表中的一个元素。它包含三部分:
- 实际的数据: 你想存储的值(比如整数、字符串、对象等)。
- 指向下一个人的指针(Next): 告诉我下一个元素是谁。
- 指向上一个人的指针(Prev): 告诉我上一个元素是谁。(这就是"双向"的关键!)
- 手拉手(指针): 这些指针就像他们互相拉着的手,把所有人连接起来,形成一个序列。
- 队伍的头尾: 队伍有明确的第一个(
front()
)和最后一个(back()
)人。
核心特性(为什么选它?)
-
插入/删除快到飞起(O(1)时间复杂度 - 常数时间):
- 场景: 你需要在队伍中间(比如人B和人C之间)插入一个新成员(人X),或者把中间的某个人(比如人M)踢出去。
- 传统数组/vector的痛: 如果用数组或
std::vector
,插入/删除中间元素,需要把后面所有人依次往后挪或往前挪,人越多,挪动越耗时(O(n)时间复杂度 - 线性时间)。想象搬砖,队伍越长越累。 - list的绝活:
list
只需要修改指针!找到人B和人C(或者找到人M),让B松开C的手,拉上X的手,同时让X拉上C的手(插入)。或者让M前面的人松开M的手,直接拉住M后面人的手,再把M请走(删除)。无论队伍多长(100人还是100万人),这个"改指针"的操作代价几乎不变! 这是list
最大的优势。
-
不连续的内存(非连续存储):
- 场景: 队伍里的人不需要紧挨着站在一块连续的空地上。他们可以分散在操场各处,只要互相记得谁在前后就行。
- 好处: 插入删除时不需要大块连续内存移动,效率高(如上所述)。
- 代价:
- 不能随机访问(Random Access): 你不能直接说"我要第5个人!"。队伍里的人只知道邻居是谁,不知道自己是第几个。想找第5个人?你得从第1个开始,一个接一个地数(顺着指针找),直到第5个(O(n)时间)。
vector
像一排编号的储物柜,可以直接走到第5号(O(1)时间)。 - 内存开销稍大: 每个人除了要带自己的行李(数据),还要带两张写着邻居地址的小纸条(前后指针),所以占用的总内存通常比
vector
只带行李要大一点。 - 可能的内存碎片: 人分散站着,时间长了操场(内存)上可能留下很多小块的空隙,虽然现代内存管理很聪明,但这在极端情况下可能是个小问题。
- 不能随机访问(Random Access): 你不能直接说"我要第5个人!"。队伍里的人只知道邻居是谁,不知道自己是第几个。想找第5个人?你得从第1个开始,一个接一个地数(顺着指针找),直到第5个(O(n)时间)。
-
迭代器稳定性(超级重要!):
- 场景: 你正在挨个检查队伍里的人(使用迭代器遍历)。这时候,有人插入或删除了,只要不是删除你当前正在看的这个人,指向其他人的迭代器仍然有效!
- 对比vector的痛: 在
vector
中间插入或删除元素,可能导致后面所有元素内存地址改变(挪位置了),之前获取的所有迭代器、指针、引用都可能瞬间失效! 继续用它们会引发崩溃或错误(悬垂指针/迭代器)。 - list的妙处: 因为
list
只改指针,元素本身待在原地不动(内存地址不变),所以只要你不删除当前元素,指向其他元素的迭代器、指针、引用稳如泰山。这在需要长时间持有迭代器或在遍历中修改结构(除了当前元素)的场景下非常关键。
基本操作(怎么用?)
cpp
#include
int main() {
// 1. 创建一个空队伍
std::list myList;
// 2. 在队伍末尾加人 (入队尾)
myList.push_back(10);
myList.push_back(20); // 队伍: 10 <--> 20
// 3. 在队伍开头加人 (插队头)
myList.push_front(5); // 队伍: 5 <--> 10 <--> 20
// 4. 在中间插人 (需要先找到位置 - 用迭代器)
// 假设我们想在10后面插入15
auto it = std::find(myList.begin(), myList.end(), 10); // 找到值为10的"人"的位置
if (it != myList.end()) {
myList.insert(++it, 15); // 在10后面(也就是++it指向的位置)插入15
// 现在队伍: 5 <--> 10 <--> 15 <--> 20
}
// 5. 删除队伍开头的人 (出队头)
myList.pop_front(); // 删除5, 队伍: 10 <--> 15 <--> 20
// 6. 删除队伍末尾的人 (出队尾)
myList.pop_back(); // 删除20, 队伍: 10 <--> 15
// 7. 删除中间特定的人 (用迭代器指定位置)
it = std::find(myList.begin(), myList.end(), 15);
if (it != myList.end()) {
myList.erase(it); // 删除15, 队伍只剩: 10
}
// 8. 获取队伍头和尾的人 (查看但不删除)
int first = myList.front(); // first = 10
int last = myList.back(); // last = 10 (因为只剩一个)
// 9. 遍历队伍 (必须用迭代器,不能直接用下标[])
std::cout << "队伍成员: ";
for (auto iter = myList.begin(); iter != myList.end(); ++iter) {
std::cout << *iter << " "; // 解引用迭代器获取数据
}
std::cout << std::endl; // 输出: 队伍成员: 10
// 10. 检查队伍是否为空
if (!myList.empty()) {
std::cout << "队伍不是空的,有 " << myList.size() << " 个人。" << std::endl;
}
// 11. 清空队伍
myList.clear(); // 队伍解散了
return 0;
}
关键成员函数(工具包)
- 构造/赋值:
list()
: 创建空链表list(n, value)
: 创建有n
个元素,每个都是value
的链表list(begin_iter, end_iter)
: 用另一个容器的迭代器范围拷贝构造list(other_list)
: 拷贝构造函数operator=
: 赋值assign()
: 替换内容
- 元素访问:
front()
: 返回第一个元素的引用 (不能用于空链表)back()
: 返回最后一个元素的引用 (不能用于空链表)- 注意: 没有
operator[]
和at()
!因为不能随机访问。
- 迭代器:
begin() / cbegin()
: 指向第一个元素的迭代器 (const)end() / cend()
: 指向最后一个元素下一个位置的迭代器 (const)rbegin() / crbegin()
: 指向最后一个元素的反向迭代器 (const)rend() / crend()
: 指向第一个元素前一个位置 的反向迭代器 (const)
- 容量:
empty()
: 检查是否为空size()
: 返回元素个数 (注意: 某些实现中size()
可能是 O(n) 操作!C++11 标准要求是 O(1),主流现代编译器都遵守)
- 修改器 (核心优势区):
push_front(value)
: 在头部插入元素pop_front()
: 删除头部元素 (不能用于空链表)push_back(value)
: 在尾部插入元素pop_back()
: 删除尾部元素 (不能用于空链表)insert(iterator pos, value)
: 在迭代器pos
指向的位置之前 插入value
,返回指向新元素的迭代器 (O(1))insert(iterator pos, n, value)
: 插入n
个value
insert(iterator pos, begin_iter, end_iter)
: 插入另一个容器的迭代器范围erase(iterator pos)
: 删除迭代器pos
指向的元素,返回指向下一个元素的迭代器 (O(1))erase(iterator first, iterator last)
: 删除[first, last)
范围的元素clear()
: 清空链表swap(other_list)
: 交换两个链表的内容 (非常快,只交换内部指针)
- 操作 (链表特有):
splice(iterator pos, other_list)
: 把other_list
的所有元素 移动到pos
指向的位置之前,other_list
变空。(O(1) 或 O(n),取决于是否同一个链表)splice(iterator pos, other_list, iterator it)
: 把other_list
中it
指向的单个元素 移动到pos
指向的位置之前。(O(1))splice(iterator pos, other_list, iterator first, iterator last)
: 把other_list
中[first, last)
范围的元素移动到pos
指向的位置之前。(O(1))remove(const T& value)
: 删除所有值等于value
的元素。(O(n))remove_if(Predicate pred)
: 删除所有满足谓词pred
的元素。(O(n))unique()
: 删除连续重复的元素(通常需要先sort
)。(O(n))unique(BinaryPredicate pred)
: 用自定义比较删除连续"重复"元素。merge(other_list)
: 合并两个已排序 链表。other_list
的内容被合并到*this
中并变空。合并后*this
保持有序。(O(n))merge(other_list, Compare comp)
: 用自定义比较合并。sort()
: 对链表进行排序(通常用归并排序)。(O(n log n))sort(Compare comp)
: 用自定义比较排序。reverse()
: 反转链表中元素的顺序。(O(n))
何时使用 std::list
?(选型建议 - 后端视角)
-
频繁在序列中间进行插入或删除操作: 这是
list
的王牌场景。比如:- 实现 LRU (Least Recently Used) 缓存淘汰算法(移动热门项到链表头/删除尾)。
- 维护一个有序列表(但插入位置不确定),需要不断插入新项到正确位置。
- 需要高效地拼接(
splice
)或分割链表。 - 需要稳定迭代器,在遍历过程中插入/删除其他元素(比如事件处理循环)。
-
对迭代器/指针/引用的稳定性要求极高: 当元素插入删除后,你还需要保证之前获取到的(指向未删除元素的)迭代器/指针/引用仍然有效时,
list
是首选。 -
不需要随机访问(即不需要按索引快速访问): 如果你大部分操作是按顺序访问或者只关心头尾,
list
是合适的。
何时避免使用 std::list
?(选型建议 - 后端视角)
- 需要频繁随机访问元素(按索引访问): 用
vector
或deque
。list
的 O(n) 随机访问太慢了。 - 内存非常紧张:
list
每个元素都有额外开销(两个指针)。如果存储的是小对象(比如int
,char
),这个开销比例会很大。vector
的内存利用率通常更高(连续存储,只有少量额外管理开销)。 - 缓存友好性(Cache Locality)至关重要:
vector
的元素在内存中连续存放,CPU 缓存预取效率极高,访问速度快。list
的元素分散存放,缓存命中率低,遍历速度可能显著慢于vector
,即使时间复杂度一样是 O(n)。 - 元素很小且操作简单:
vector
的插入删除成本在数据量不大时可能可以接受,而其随机访问和遍历速度的优势更明显。
list
vs vector
vs deque
速查表 (后端选型参考)
特性 | std::list (双向链表) |
std::vector (动态数组) |
std::deque (双端队列) |
---|---|---|---|
底层结构 | 非连续,双向链表 | 连续存储 | 分段连续 (多块数组+索引) |
随机访问 | 慢 (O(n)) ❌ | 极快 (O(1)) ✅ | 快 (O(1)) ✅ |
头部插入/删除 | 极快 (O(1)) ✅ | 慢 (O(n)) ❌ | 快 (O(1)) ✅ |
尾部插入/删除 | 极快 (O(1)) ✅ | 快 (O(1) 摊还) ✅ | 快 (O(1)) ✅ |
中间插入/删除 | 快 (O(1) 找到位置后) ✅ | 慢 (O(n)) ❌ | 慢 (O(n)) ❌ |
迭代器失效 | 高稳定 (只删当前元素失效) | 低稳定 (插入删除常失效) | 中稳定 (中间插入删除失效) |
内存开销 | 较高 (每个元素+2指针) | 较低 (少量额外管理开销) | 中 (管理索引开销) |
内存连续性 | 碎片化 | 高度连续 | 分段连续 |
缓存友好性 | 差 (遍历慢) | 极好 (遍历极快) | 较好 (分段连续) |
特殊操作 | 高效 splice , merge , sort |
- | - |
典型后端用例 | LRU缓存、需稳定迭代器的列表 | 主容器、需随机访问/遍历的集合 | 队列(FIFO)、栈(LIFO) |
性能建议
- 优先用
vector
: 默认首选。除非有明确证据(性能分析)表明list
或deque
在特定操作上显著更好。 - 善用
reserve()
(vector
/deque
): 如果能预估大小,提前预留空间,避免频繁扩容复制。 - 理解
list::size()
: 确认你的编译器/标准库实现它是 O(1) 还是 O(n)(C++11 后都是 O(1),但老代码/特定平台需留意)。如果需要频繁调用且是 O(n),考虑自己维护计数器。 - 考虑
deque
做队列/栈: 如果主要操作在头尾,deque
通常比list
更优(内存更紧凑,缓存更好,同样 O(1) 操作)。
总结
std::list
是一个强大的双向链表 容器。它在序列中间频繁插入和删除操作上效率无敌(O(1)) ,并且提供了卓越的迭代器稳定性 。它还支持高效的链表特有操作如 splice
, merge
, sort
。
但是,它不擅长随机访问(O(n)) ,内存开销相对较大 ,并且缓存不友好。
作为后端工程师,选择它时要非常明确:你的场景是否极度依赖中间的高效插入删除?是否需要超强的迭代器稳定性?如果答案是肯定的,并且可以接受随机访问慢和内存稍大的代价,那么 list
就是你的神兵利器。否则,vector
或 deque
通常是更通用、性能更均衡的选择。 理解每种容器的底层实现和性能特征是做出明智选择的关键。
下面我将详细讲解 std::list
中的各个函数,包括使用示例和注意事项。
1. 构造函数
cpp
// 默认构造
std::list<int> list1; // 空list
// 填充构造 (n个相同元素)
std::list<int> list2(5, 100); // {100, 100, 100, 100, 100}
// 范围构造 (用迭代器范围)
std::vector<int> vec = {1, 2, 3};
std::list<int> list3(vec.begin(), vec.end()); // {1, 2, 3}
// 拷贝构造
std::list<int> list4(list3); // {1, 2, 3}
注意事项:
- 默认构造创建空链表,内存开销最小
- 填充构造的
val
参数可省略(使用类型默认值) - 范围构造兼容任何迭代器(数组、vector等)
2. 容量相关
(1) empty()
cpp
std::list<int> list;
if (list.empty()) { // 返回 true
std::cout << "List is empty!\n";
}
(2) size()
cpp
std::list<int> list = {1, 2, 3};
std::cout << "Size: " << list.size(); // 输出 3
(3) max_size()
cpp
std::list<int> list;
std::cout << "Max possible size: " << list.max_size();
// 输出理论最大容量(通常极大)
注意事项:
empty()
比size()==0
更高效size()
在C++11后保证是O(1)时间复杂度max_size()
是理论值,实际受内存限制
3. 元素访问
(1) front()
cpp
std::list<std::string> words = {"apple", "banana"};
std::cout << words.front(); // 输出 "apple"
words.front() = "orange"; // 修改第一个元素
(2) back()
cpp
std::list<int> nums = {10, 20, 30};
std::cout << nums.back(); // 输出 30
nums.back() += 5; // 修改为35
注意事项:
- 在空list上调用会导致未定义行为
- 返回引用,可直接修改元素
- 比迭代器访问更简洁
4. 修改器
(1) assign()
- 重新赋值
cpp
std::list<char> letters;
// 方法1:n个相同值
letters.assign(3, 'A'); // {'A','A','A'}
// 方法2:迭代器范围
std::string word = "XYZ";
letters.assign(word.begin(), word.end()); // {'X','Y','Z'}
(2) emplace_front()
- 头部构造插入(C++11)
cpp
struct Point { int x, y; Point(int a, int b) : x(a), y(b) {} };
std::list<Point> points;
points.emplace_front(1, 2); // 直接构造,避免拷贝
(3) push_front()
- 头部插入
cpp
std::list<int> nums;
nums.push_front(3); // {3}
nums.push_front(1); // {1, 3}
(4) pop_front()
- 头部删除
cpp
std::list<int> nums = {1, 2, 3};
nums.pop_front(); // {2, 3}
(5) emplace_back()
- 尾部构造插入(C++11)
cpp
std::list<std::pair<int, int>> pairs;
pairs.emplace_back(4, 5); // 直接构造 pair(4,5)
(6) push_back()
- 尾部插入
cpp
std::list<std::string> names;
names.push_back("Alice");
names.push_back("Bob"); // {"Alice", "Bob"}
(7) pop_back()
- 尾部删除
cpp
std::list<int> nums = {1, 2, 3};
nums.pop_back(); // {1, 2}
(8) emplace()
- 指定位置构造插入(C++11)
cpp
std::list<int> nums = {10, 30};
auto it = nums.begin();
++it; // 指向30
nums.emplace(it, 20); // 在30前插入20 → {10,20,30}
(9) insert()
- 指定位置插入
cpp
std::list<int> nums = {1, 4};
// 插入单个元素
auto it = nums.begin();
++it;
it = nums.insert(it, 2); // {1,2,4},返回指向2的迭代器
// 插入多个相同元素
nums.insert(it, 2, 3); // {1,3,3,2,4}
// 插入迭代器范围
std::vector<int> vec = {5,6};
nums.insert(nums.end(), vec.begin(), vec.end()); // 尾部追加
(10) erase()
- 删除元素
cpp
std::list<int> nums = {10, 20, 30, 40};
auto it = nums.begin();
++it; // 指向20
// 删除单个元素
it = nums.erase(it); // 删除20,it现在指向30
// 删除范围
auto first = nums.begin();
auto last = nums.end();
--last;
nums.erase(first, last); // 删除[第一个, 最后一个) → 只剩40
(11) swap()
- 交换内容
cpp
std::list<int> listA = {1, 2};
std::list<int> listB = {3, 4, 5};
listA.swap(listB);
// listA = {3,4,5}, listB = {1,2}
(12) resize()
- 调整大小
cpp
std::list<int> nums = {1, 2, 3};
nums.resize(5); // {1,2,3,0,0} 默认填充0
nums.resize(2); // {1,2} 删除多余元素
nums.resize(4, 99); // {1,2,99,99} 指定填充值
(13) clear()
- 清空内容
cpp
std::list<int> nums = {1, 2, 3};
nums.clear(); // 变为空list
修改器注意事项:
-
所有插入/删除操作均保持迭代器有效性(被删除元素除外)
-
emplace
系列函数效率高于push
(避免拷贝) -
erase
返回被删除元素的下一个有效迭代器 -
resize
缩小尺寸会销毁多余元素 -
clear
不会释放内存(capacity不变),可用swap
技巧释放内存:cppstd::list<int>().swap(nums); // 释放所有内存
5. 特殊操作
(1) splice()
- 链表拼接
cpp
std::list<int> listA = {1, 2};
std::list<int> listB = {3, 4};
// 1. 移动整个list
listA.splice(listA.end(), listB);
// listA={1,2,3,4}, listB为空
// 2. 移动单个元素
listB.splice(listB.begin(), listA, listA.begin());
// listB={1}, listA={2,3,4}
// 3. 移动元素范围
auto it = listA.begin();
++it; // 指向3
listB.splice(listB.end(), listA, it, listA.end());
// listB={1,3,4}, listA={2}
(2) remove()
- 按值删除
cpp
std::list<int> nums = {1, 2, 3, 2, 4};
nums.remove(2); // 删除所有2 → {1,3,4}
(3) remove_if()
- 条件删除
cpp
std::list<int> nums = {1, 2, 3, 4, 5};
nums.remove_if([](int n) {
return n % 2 == 0; // 删除所有偶数
}); // {1,3,5}
(4) unique()
- 去重
cpp
std::list<int> nums = {1, 2, 2, 3, 3, 3};
nums.unique(); // 删除连续重复 → {1,2,3}
// 自定义去重规则(相邻元素差<=1)
nums = {1, 2, 4, 5, 7};
nums.unique([](int a, int b) {
return std::abs(a - b) <= 1;
}); // 结果:{1,4,7}(2,5被删除)
(5) merge()
- 合并有序列表
cpp
std::list<int> listA = {1, 3, 5};
std::list<int> listB = {2, 4, 6};
// 前提:两个list必须已排序!
listA.merge(listB);
// listA={1,2,3,4,5,6}, listB为空
// 自定义排序规则(降序)
listA = {5, 3, 1};
listB = {6, 4, 2};
listA.merge(listB, std::greater<int>());
// listA={6,5,4,3,2,1}
(6) sort()
- 排序
cpp
std::list<int> nums = {3, 1, 4, 2};
// 默认升序
nums.sort(); // {1,2,3,4}
// 自定义排序(降序)
nums.sort(std::greater<int>()); // {4,3,2,1}
(7) reverse()
- 反转
cpp
std::list<int> nums = {1, 2, 3};
nums.reverse(); // {3,2,1}
特殊操作注意事项:
-
splice
:- 最强大的链表操作,时间复杂度O(1)
- 不复制元素,只修改指针
- 源链表会被修改
-
merge
:- 必须保证两个链表已排序
- 使用与排序时相同的比较规则
- 合并后源链表被清空
-
sort
:- 链表专用排序(通常为归并排序)
- 比
std::sort
更高效(因std::sort
需要随机访问)
-
unique
:- 只删除连续重复元素
- 通常先
sort
再unique
实现完全去重
关键总结
操作类型 | 推荐函数 | 使用场景 |
---|---|---|
头部操作 | emplace_front /push_front |
实现队列(FIFO)时使用 |
尾部操作 | emplace_back /push_back |
实现栈(LIFO)时使用 |
位置插入 | emplace /insert |
需要中间插入时使用 |
高效转移 | splice |
合并链表无需拷贝 |
条件删除 | remove_if |
根据复杂条件删除元素 |
排序去重 | sort + unique |
准备数据时使用 |
最佳实践:
-
需要频繁中间插入/删除 → 首选
list
-
操作元素前检查
empty()
避免未定义行为 -
尽量用
emplace
替代push
提升性能 -
合并有序列表时优先用
merge
而非insert
-
清空内存技巧:
cppstd::list<int> temp; nums.swap(temp); // 彻底释放内存
通过结合具体场景选择合适的操作,可以充分发挥链表优势,实现高效的数据处理。