C++笔记——STL list
1. list 基本概念
什么是list?
双向链表:每个元素包含指向前后元素的指针,形成链式结构
核心特性(与vector/deque对比):
| 特性 | vector | deque | list |
|---|---|---|---|
| 随机访问 | ✅ O(1) | ✅ O(1) | ❌ O(n) |
| 头部插入删除 | ❌ O(n) | ✅ O(1) | ✅ O(1) |
| 尾部插入删除 | ✅ O(1) | ✅ O(1) | ✅ O(1) |
| 中间插入删除 | ❌ O(n) | ❌ O(n) | ✅ O(1) |
| 内存布局 | 连续 | 分段连续 | 完全分散 |
2. list 的基本使用
头文件和声明:
cpp
#include <list>
using namespace std;// 创建方式(与其他容器类似)
list<int> lst1; // 空list
list<int> lst2(10); // 10个元素,默认值0
list<int> lst3(5, 100); // 5个元素,每个都是100
list<int> lst4 = {1, 2, 3, 4, 5}; // 初始化列表
list<int> lst5(lst4); // 拷贝构造3. list 的特有操作
高效的任意位置操作:
cpp
void list_special_operations() {list<int> lst = {1, 3, 5};// 任意位置高效插入(与vector/deque的最大区别!)auto it = lst.begin();advance(it, 1); // 移动到第2个位置lst.insert(it, 2); // 在位置1插入2: {1,2,3,5} - O(1)!// 任意位置高效删除it = lst.begin();advance(it, 2); // 移动到第3个位置 lst.erase(it); // 删除3: {1,2,5} - O(1)!// 头部尾部操作(与deque相同)lst.push_front(0); // 头部插入: {0,1,2,5}lst.push_back(6); // 尾部插入: {0,1,2,5,6}lst.pop_front(); // 头部删除: {1,2,5,6}lst.pop_back(); // 尾部删除: {1,2,5}
}4. list 的容量管理
与vector/deque的对比:
cpp
void list_capacity() {list<int> lst = {1, 2, 3, 4, 5};// 这些与其他容器相同:cout << "大小: " << lst.size() << endl; // 5cout << "是否为空: " << lst.empty() << endl; // falselst.resize(10); // 扩容到10个元素lst.resize(3); // 缩容到3个元素// list特有的容量操作:cout << "最大大小: " << lst.max_size() << endl;// 但没有这些(因为链表不需要):// lst.capacity(); // ❌ // lst.reserve(100); // ❌// lst.shrink_to_fit();// ❌
}5. list 的元素访问
这是与vector/deque的最大区别!
cpp
void list_access() {list<int> lst = {10, 20, 30, 40, 50};// 只能顺序访问,不能随机访问!cout << lst.front() << endl; // 10 - 头部元素 ✅cout << lst.back() << endl; // 50 - 尾部元素 ✅// 这些都不行!// cout << lst[2] << endl; // ❌ 没有下标访问!// cout << lst.at(2) << endl; // ❌ 没有at()方法!// int* data = lst.data(); // ❌ 没有data()方法!// 访问中间元素的正确方式:auto it = lst.begin();advance(it, 2); // 移动到第3个位置 - O(n)!cout << *it << endl; // 30
}访问方式对比:
cpp
void access_comparison() {vector<int> vec = {1,2,3,4,5};list<int> lst = {1,2,3,4,5};// vector - 随机访问cout << vec[2] << endl; // ✅ O(1) - 直接计算地址// list - 顺序访问 auto it = lst.begin();advance(it, 2); // ✅ O(n) - 需要遍历节点cout << *it << endl;
}6. list 的修改操作
链表特有的高效操作:
cpp
void list_modifications() {list<int> lst = {1, 2, 3, 4, 5};// 1. 拼接操作 - 链表特有!list<int> other = {6, 7, 8};lst.splice(lst.end(), other); // 把other拼接到lst末尾// lst: {1,2,3,4,5,6,7,8}, other: 空// O(1)操作!只是修改指针// 2. 元素移动 - 链表特有!auto it = find(lst.begin(), lst.end(), 3);lst.splice(lst.begin(), lst, it); // 把3移动到开头// lst: {3,1,2,4,5,6,7,8} - O(1)!
}7. list 的迭代器
双向迭代器(不是随机访问!):
cpp
void list_iterators() {list<int> lst = {10, 20, 30, 40, 50};// 正向遍历 ✅for (auto it = lst.begin(); it != lst.end(); ++it) {cout << *it << " ";}// 反向遍历 ✅ for (auto rit = lst.rbegin(); rit != lst.rend(); ++rit) {cout << *rit << " ";}// 这些都不行! ❌auto it = lst.begin();// cout << it[2] << endl; // 没有随机访问// it = it + 3; // 没有算术运算// if (it1 < it2) { } // 没有比较位置// 只能这样移动:advance(it, 2); // O(n) - 需要一步步走// 或者:++it; ++it; // 也是O(n)
}迭代器稳定性(重要优势!):
cpp
void iterator_stability() {list<int> lst = {1, 2, 3, 4, 5};auto it = lst.begin();advance(it, 2); // 指向3cout << "当前指向: " << *it << endl; // 3// list的迭代器在插入删除时不会失效!lst.push_front(0); // 头部插入lst.push_back(6); // 尾部插入lst.insert(lst.begin(), 99); // 任意位置插入cout << "插入后仍然指向: " << *it << endl; // 还是3! ✅// 即使删除其他元素,迭代器仍然有效lst.erase(lst.begin()); // 删除头部cout << "删除后仍然指向: " << *it << endl; // 还是3! ✅// 只有删除迭代器指向的元素时才会失效// lst.erase(it); // 此时it失效,不能再使用
}这是list相比vector/deque的巨大优势!
8. list 的特有算法
成员函数版本的算法(比通用算法更高效):
cpp
void list_algorithms() {list<int> lst = {5, 2, 8, 2, 1, 2, 9, 3};// 1. 排序 - 成员函数版本lst.sort(); // 原地排序: {1,2,2,2,3,5,8,9}// 比std::sort更高效,因为专门为链表优化// 2. 去重 - 需要先排序lst.unique(); // 去除连续重复: {1,2,3,5,8,9}// 3. 合并 - 链表特有操作list<int> lst2 = {4, 6, 7};lst2.sort(); // 必须先排序lst.merge(lst2); // 合并两个有序链表// lst: {1,2,3,4,5,6,7,8,9}, lst2: 空// 4. 反转lst.reverse(); // 反转链表: {9,8,7,6,5,4,3,2,1}
}为什么需要成员函数版本?
cpp
void why_member_algorithms() {list<int> lst = {3, 1, 4, 2};// 错误:std::sort需要随机访问迭代器// sort(lst.begin(), lst.end()); // ❌ 编译错误!// 正确:使用list的成员函数lst.sort(); // ✅ 专门为链表实现的排序算法
}9. list 的内部实现原理
节点结构:
cpp
// list的每个节点大致如下:
struct ListNode {T data; // 存储的数据ListNode* prev; // 指向前一个节点ListNode* next; // 指向后一个节点
};内存布局:
text
list: [头节点] ↔ [节点1] ↔ [节点2] ↔ [节点3] ↔ ... ↔ [尾节点]↑ ↑ ↑ ↑prev/next prev/next prev/next prev/next
这种设计的优势:
cpp
void list_advantages() {list<int> lst;// 1. 任意位置插入删除都是O(1)for (int i = 0; i < 1000; i++) {auto it = lst.begin();advance(it, i % (lst.size() + 1));lst.insert(it, i); // 在任意位置插入 - 总是O(1)!}// 2. 不需要重新分配内存// 每个新元素独立分配节点,不会导致已有元素移动
}10. 性能特点
时间复杂度:
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 头部插入删除 | O(1) | 修改指针 |
| 尾部插入删除 | O(1) | 修改指针 |
| 中间插入删除 | O(1) | 找到位置后修改指针 |
| 随机访问 | O(n) | 需要从头遍历 |
| 查找 | O(n) | 顺序查找 |
空间开销:
cpp
void space_overhead() {list<int> lst = {1, 2, 3};// 每个元素都有额外开销:// - 指向前一个节点的指针// - 指向后一个节点的指针 // - 内存分配开销(每个节点独立分配)// 对于int类型,额外开销可能比数据本身还大!// 在64位系统上:int(4字节) + 两个指针(16字节) = 20字节/元素
}11. list 的典型应用场景
适合使用list的场景:
1. 频繁在任意位置插入删除
cpp
class TextEditor {
private:list<char> text;public:void insert(char c, size_t position) {auto it = text.begin();advance(it, position);text.insert(it, c); // O(1)插入!}void deleteChar(size_t position) {auto it = text.begin();advance(it, position);text.erase(it); // O(1)删除!}void display() {for (char c : text) cout << c;cout << endl;}
};2. LRU缓存实现
cpp
class LRUCache {
private:list<pair<int, int>> cache; // (key, value)对unordered_map<int, list<pair<int, int>>::iterator> map;int capacity;public:LRUCache(int cap) : capacity(cap) {}int get(int key) {if (map.find(key) == map.end()) return -1;// 移动到头部(最近使用)cache.splice(cache.begin(), cache, map[key]);return map[key]->second;}void put(int key, int value) {if (map.find(key) != map.end()) {// 键已存在,更新值并移动到头部map[key]->second = value;cache.splice(cache.begin(), cache, map[key]);} else {// 新键,插入到头部if (cache.size() == capacity) {// 删除最久未使用的(尾部)int lastKey = cache.back().first;cache.pop_back();map.erase(lastKey);}cache.emplace_front(key, value);map[key] = cache.begin();}}
};3. 音乐播放列表
cpp
class Playlist {
private:list<string> songs;list<string>::iterator current;public:Playlist(const vector<string>& initialSongs) {songs.assign(initialSongs.begin(), initialSongs.end());current = songs.begin();}void addSong(const string& song, bool playNext = false) {if (playNext) {current = songs.insert(next(current), song);} else {songs.push_back(song);}}void removeCurrent() {current = songs.erase(current);if (current == songs.end()) current = songs.begin();}void next() {if (next(current) != songs.end()) ++current;}void previous() {if (current != songs.begin()) --current;}
};12. 注意事项和最佳实践
1. 避免频繁的随机访问
cpp
void bad_practice() {list<int> lst(1000);// 错误:频繁随机访问for (int i = 0; i < lst.size(); i++) {auto it = lst.begin();advance(it, i); // 每次都是O(n)!cout << *it << " "; // 总时间复杂度O(n²)!}// 正确:顺序访问for (auto it = lst.begin(); it != lst.end(); ++it) {cout << *it << " "; // 总时间复杂度O(n)}
}2. 利用迭代器稳定性
cpp
void use_iterator_stability() {list<int> lst = {1, 2, 3, 4, 5};vector<list<int>::iterator> important_positions;// 保存重要位置的迭代器auto it = lst.begin();advance(it, 2);important_positions.push_back(it); // 指向3// 进行各种修改操作...lst.push_front(0);lst.push_back(6);lst.insert(lst.begin(), 99);// 迭代器仍然有效!cout << *important_positions[0] << endl; // 还是3!
}13. 总结
list的核心优势:
任意位置O(1)插入删除
迭代器稳定性 - 插入删除不影响其他迭代器
不需要连续内存
特有的高效链表算法
list的缺点:
没有随机访问 - 只能顺序访问
内存开销大 - 每个元素都有指针开销
缓存不友好 - 内存不连续
以下是一些 std::list 的主要应用场景:
(1)频繁插入和删除操作:std::list 允许在序列中的任意位置进行常数时间的插入和删除操作。这使得它非常适合需要频繁添加或移除元素的场景,如日志记录、任务队列、事件处理等。
(2)双向迭代:std::list 支持双向迭代,这意味着可以轻松地遍历列表,向前或向后移动。这种特性在处理需要前后关联的数据时非常有用,如文本编辑器中的撤销与重做操作。
(3)保持元素顺序:std::list 通过链表结构保持元素的插入顺序。这对于需要维护元素原始顺序的场景(如数据库查询结果、事件历史记录等)非常有用。
(4)动态数据结构:当数据结构的大小经常变化时,std::list 是一个很好的选择。与基于数组的容器(如 std::vector)相比,std::list 在插入和删除元素时不需要移动大量数据,因此性能更高。
(5)内存管理:在某些情况下,可能希望更好地控制内存使用。std::list 允许管理每个元素的内存分配,这在处理大量数据或需要精确控制内存使用的场景中非常有用。
选择指南:
使用 list 当:
✅ 需要频繁在任意位置插入删除
✅ 需要稳定的迭代器
✅ 实现LRU缓存、undo操作等场景
✅ 内存碎片化严重
使用 vector/deque 当:
✅ 需要随机访问
✅ 主要进行尾部操作
✅ 重视缓存性能和内存效率
性能对比总结:
| 操作 | vector | deque | list |
|---|---|---|---|
| 尾部插入删除 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 头部插入删除 | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 中间插入删除 | ⭐ | ⭐ | ⭐⭐⭐⭐⭐ |
| 随机访问 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |
| 内存效率 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
1)vector 底层实现是数组;list 是双向链表。
2)vector 支持随机访问,list 不支持。
3)vector 是顺序内存,list 不是。
4)vector 在中间节点进行插入删除会导致内存拷贝,list 不会。
5)vector 一次性分配好内存,不够时才进行 2 倍扩容(或 1.5 倍);list 每次插入新节点都会进行内存申请。
6)vector 随机访问性能好,插入删除性能差;list 随机访问性能差,插入删除性能好
一句话总结:
list = 任意位置高效操作 + 稳定迭代器 - 随机访问能力
list在特定场景下是不可替代的工具,理解它的特性可以让你在合适的时候做出最佳选择!


