一、先想清楚:你真的需要list吗?
在写代码前,先问自己一个问题:我需要的是“频繁插入删除”还是“随机访问”?
list和vector是STL中最常用的两个序列容器,但底层实现天差地别——我们用一张表把核心区别讲清楚:

特性 | list(双向链表) | vector(动态数组) |
---|---|---|
存储方式 | 非连续内存(节点+指针) | 连续内存(数组扩容) |
插入/删除效率 | 任意位置O(1)(只需调整指针) | 中间位置O(n)(需移动元素) |
随机访问 | 不支持(不能用[] 或at() ) |
支持(O(1)时间访问任意位置) |
内存开销 | 额外存储指针(每个节点占更多内存) | 内存连续,开销小 |
迭代器类型 | 双向迭代器(只能++/–) | 随机访问迭代器(支持+=n) |
举个例子:如果你要写一个“聊天记录”功能,需要频繁在消息列表头部插入新消息(比如置顶),或者删除中间某条消息——list就是最优解,因为插入删除不会移动其他元素;但如果是“成绩排行榜”,需要频繁按索引取第n名成绩,那vector更合适。
二、list的基础操作:从构造到遍历
1. 怎么构造一个list?
list的构造函数很灵活,直接看常用场景和代码:
构造方式 | 代码示例 | 说明 |
---|---|---|
默认构造 | list<int> l; |
创建空list |
n个相同元素 | list<int> l(5, 10); |
创建5个元素,每个都是10 |
范围构造(用其他容器) | vector<int> v{1,2,3}; list<int> l(v.begin(), v.end()); |
用vector的范围构造list |
拷贝构造 | list<int> l2(l1); |
复制l1的所有元素到l2 |
移动构造(C++11+) | list<int> l3(std::move(l2)); |
转移l2的资源到l3(l2变为空) |
2. 插入与删除:最常用的5个操作
list的插入删除是“拿手好戏”,直接上代码:
#include <list>
#include <iostream>
using namespace std;
int main() {
list<int> l;
// 1. 尾部插入
l.push_back(1); // l: [1]
// 2. 头部插入
l.push_front(0); // l: [0,1]
// 3. 中间插入(用迭代器定位)
auto it = l.begin(); ++it; // 指向1的位置
l.insert(it, 2); // l: [0,2,1](在it前插入2)
// 4. 删除尾部
l.pop_back(); // l: [0,2]
// 5. 删除指定位置
it = l.begin();
l.erase(it); // l: [2](删除头部元素)
return 0;
}
注意:insert
的参数是“插入位置的迭代器”,插入的元素会放在该迭代器前面;erase
会返回下一个有效的迭代器(这点很重要,后面讲迭代器失效时会用到)。
3. 怎么遍历list?
list不支持[]
访问,所以遍历只能用迭代器或范围for循环(C++11+):
list<int> l{1,2,3,4};
// 方式1:迭代器遍历(最通用)
for (list<int>::iterator it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 输出:1 2 3 4
}
// 方式2:范围for(更简洁)
for (auto num : l) {
cout << num << " ";
}
// 方式3:反向迭代器(从后往前)
for (auto rit = l.rbegin(); rit != l.rend(); ++rit) {
cout << *rit << " "; // 输出:4 3 2 1
}
三、list的高级玩法:迭代器、排序与合并
1. 迭代器的“坑”:失效与正确用法
list的迭代器是双向迭代器,只能向前或向后移动,但不会因为插入操作失效(除非你删除了迭代器指向的节点)。比如:
list<int> l{1,2,3};
auto it = l.begin(); // 指向1
l.insert(it, 0); // 插入0,it仍然指向1(有效!)
cout << *it << endl; // 输出1(正确)
但删除操作会让迭代器失效,所以删除元素时要用erase的返回值更新迭代器:
// 错误示例:删除所有偶数(会崩溃!)
for (auto it = l.begin(); it != l.end(); ++it) {
if (*it % 2 == 0) {
l.erase(it); // erase后it失效,++it会出错
}
}
// 正确示例:用erase的返回值
for (auto it = l.begin(); it != l.end(); ) { // 注意:这里没有++it
if (*it % 2 == 0) {
it = l.erase(it); // erase返回下一个有效迭代器
} else {
++it; // 只有不删除时才移动迭代器
}
}
2. 排序:list自带的sort()
list不能用std::sort
(因为std::sort
需要随机访问迭代器),但list自己有成员函数sort(),用法很简单:
list<int> l{3,1,4,1,5};
// 默认升序排序
l.sort(); // l: [1,1,3,4,5]
// 自定义降序排序(用lambda)
l.sort([](int a, int b) { return a > b; }); // l: [5,4,3,1,1]
小技巧:list的sort是稳定排序(相同元素的相对位置不变),而std::sort
是不稳定的——如果你的场景需要保持相同元素的顺序(比如“按时间排序的消息列表,相同时间的消息保持原顺序”),list的sort更合适。
3. 合并:把两个有序list合并成一个
list的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变为空
注意:merge的前提是两个list都已经按相同规则排序(比如都是升序),否则结果会错乱。
四、list的实战:用list实现LRU缓存
LRU(最近最少使用)缓存是面试高频题,而list的splice()函数(转移元素位置)是实现LRU的关键!
1. LRU的需求
- 缓存有固定容量,满了就删除“最久未使用”的元素;
- 访问(get)一个元素时,把它移到“最近使用”的位置(比如头部);
- 插入(put)一个元素时,如果存在就更新值并移到头部,如果不存在就插入头部(容量满则删尾部)。
2. 用list+哈希表实现
#include <list>
#include <unordered_map>
#include <iostream>
using namespace std;
class LRUCache {
private:
int capacity; // 缓存容量
// list存储键值对:front是最近使用,back是最久未使用
list<pair<int, int>> cache;
// 哈希表:键 -> list中的迭代器(快速找到元素位置)
unordered_map<int, list<pair<int, int>>::iterator> map;
public:
LRUCache(int cap) : capacity(cap) {}
// 获取缓存值
int get(int key) {
auto it = map.find(key);
if (it == map.end()) {
return -1; // 不存在该键
}
// 把访问的元素移到list头部(splice是list的成员函数,O(1)时间)
cache.splice(cache.begin(), cache, it->second);
return it->second->second; // 返回值
}
// 插入/更新缓存
void put(int key, int value) {
auto it = map.find(key);
if (it != map.end()) {
// 存在,更新值并移到头部
it->second->second = value;
cache.splice(cache.begin(), cache, it->second);
return;
}
// 不存在,检查容量
if (cache.size() == capacity) {
// 删除最久未使用的元素(list尾部)
auto last = cache.back();
map.erase(last.first); // 从哈希表删除键
cache.pop_back(); // 从list删除尾部
}
// 插入新元素到头部
cache.push_front({key, value});
map[key] = cache.begin(); // 哈希表记录迭代器
}
};
// 测试代码
int main() {
LRUCache cache(2);
cache.put(1, 1); // 缓存:[(1,1)]
cache.put(2, 2); // 缓存:[(2,2), (1,1)]
cout << cache.get(1) << endl; // 1,缓存变为[(1,1), (2,2)]
cache.put(3, 3); // 容量满,删除(2,2),缓存变为[(3,3), (1,1)]
cout << cache.get(2) << endl; // -1(已删除)
return 0;
}
关键亮点:cache.splice(cache.begin(), cache, it->second)
——这个操作把it->second
指向的元素移动到list的头部,没有拷贝或移动元素本身,只是调整了指针,所以是O(1)时间复杂度,完美符合LRU的性能要求!
五、常见陷阱:避开这些坑
- 不要用[]访问list:list没有重载
operator[]
,如果你写l[0]
会直接编译错误,要用l.front()
(头部)或l.back()
(尾部)。 - 迭代器失效要处理:删除元素时一定要用
it = l.erase(it)
更新迭代器,否则会访问失效内存。 - 不要用std::sort排序list:会编译错误!必须用list自带的
l.sort()
。 - splice的参数顺序:
splice(目标位置, 源list, 源迭代器)
,不要搞反顺序。 - list的size()是O(n)吗?:C++11之前,list的
size()
是O(n)(需要遍历计数),C++11之后是O(1)(存储了size变量)——如果你的编译器支持C++11+,可以放心用size()
。
最后:总结一下list的核心场景
- 需要频繁插入删除中间或头部元素时;
- 不需要随机访问元素时;
- 需要稳定排序时;
- 实现LRU、FIFO队列等数据结构时。
如果你能记住这些场景,下次写代码时就能快速判断:“哦,这个需求该用list!”
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/164