目录
[(1)节点结构:list 的 "最小单元"](#(1)节点结构:list 的 “最小单元”)
[(2)list 的整体结构:节点如何串联?](#(2)list 的整体结构:节点如何串联?)
[2. 为什么要设计 list?(核心价值:解决 vector 的致命短板)](#2. 为什么要设计 list?(核心价值:解决 vector 的致命短板))
[(1)vector 的痛点:中间增删要 "搬家"](#(1)vector 的痛点:中间增删要 “搬家”)
[(2)list 的解决方案:增删只需 "改指针"](#(2)list 的解决方案:增删只需 “改指针”)
[(3)list 的其他优势:无扩容、内存利用率高](#(3)list 的其他优势:无扩容、内存利用率高)
[3. 怎么用 list?(核心接口与限制)](#3. 怎么用 list?(核心接口与限制))
[(1)增删操作:高效的双向增删(O (1))](#(1)增删操作:高效的双向增删(O (1)))
[(2)遍历操作:只能用迭代器(不支持 [])](#(2)遍历操作:只能用迭代器(不支持 []))
[(3)其他常用接口(O (1) 或 O (n))](#(3)其他常用接口(O (1) 或 O (n)))
[4. 和 vector 比差在哪?(核心差异对比)](#4. 和 vector 比差在哪?(核心差异对比))
[5. 用 list 有什么坑?(边界与局限)](#5. 用 list 有什么坑?(边界与局限))
[(1)坑 1:随机访问效率极低,别用它做 "数组"](#(1)坑 1:随机访问效率极低,别用它做 “数组”)
[(2)坑 2:内存碎片化与指针开销](#(2)坑 2:内存碎片化与指针开销)
[(3)坑 3:迭代器不支持 "算术操作",遍历易出错](#(3)坑 3:迭代器不支持 “算术操作”,遍历易出错)
[(4)坑 4:sort () 效率不如 vector](#(4)坑 4:sort () 效率不如 vector)
前言
在 C++ STL 的序列容器中,vector
因连续内存的高效随机访问 成为 "常客",但当遇到 "频繁中间增删 " 场景时,它的性能短板会暴露 无遗。而**list
** ------ 这个基于双向链表的容器,正是为解决这一痛点而生。
1.介绍list
list的底层是双向循环链表,每个元素以 "独立节点 " 形式存在,节点间通过指针关联,内存中不连续存储。
list的结构,下图就是带有双向循环链表:

(1)节点结构:list 的 "最小单元"
一个list节点包含三个部分:数据域 (存储元素值)、前驱指针 (prev
,指向前一个节点)、后继指针 (next
,指向后一个节点)。
模拟实现的节点结构代码:
cpp
template <class T>
struct ListNode {
T data; // 存储元素的值
ListNode* prev; // 指向"前一个节点"的指针
ListNode* next; // 指向"后一个节点"的指针
// 节点构造函数:初始化数据,前后指针默认空
ListNode(const T& val) : data(val), prev(nullptr), next(nullptr) {}
};
(2)list 的整体结构:节点如何串联?
list容器通过一个 "头节点 "(_head
,不存储实际数据)和 "元素个数 "(_size
)管理整个链表。以存储{1, 2, 3,4,5}
的**list
为例,**其结构如下:
list 整体结构插图:

若为双向循环链表 (如 GCC 的 STL 实现),尾节点的next
会指向_head
,_head
的prev
会指向尾节点 ,形成闭环,好处是 "定位尾节点无需遍历,直接_head->prev
即可"。
关键区别于 vector:
- **
vector
**是 "连续一块内存",像一排紧密排列的箱子;- **
list
**是 "零散节点靠指针串起",像一串带绳子的珠子,珠子可散落在不同位置。
2. 为什么要设计 list?(核心价值:解决 vector 的致命短板)
list的存在,不是为了 "替代 vector" ,而是为了弥补 vector 在 "中间增删" 场景下的低效 ------ 这是 vector 的 "致命短板"。
(1)vector 的痛点:中间增删要 "搬家"
vector
的内存是连续的,当在中间插入 / 删除元素时,需要移动后续所有元素:
- 例:在**
vector
** 的第 100 个元素前插入新元素,需将第 100~ 末尾的所有元素向后移动 1 位(时间复杂度O(n)
);- 若**
vector
**容量不足,还需扩容(申请新内存→拷贝旧元素→释放旧内存),额外增加开销。
(2)list 的解决方案:增删只需 "改指针"
list
的节点是独立 的,插入 / 删除元素时,无需移动其他节点,只需修改相邻节点的prev
和**next
** 指针(时间复杂度O(1)
):
例 :在
list
的1
和2
之间插入3
,只需做 4 步:
新节点
3
的**prev
** 指向1
;新节点
3
的**next
** 指向2
;节点
1
的**next
** 指向3
;节点
2
的**prev
** 指向3
。
(3)list 的其他优势:无扩容、内存利用率高
无需扩容 :
list
的节点随用随申请 ,用完释放,不会像**vector
** 那样 "容量> 大小" 导致内存浪费;内存碎片化可控 :虽然节点零散,但对于 "频繁增删 " 场景**,总内存开销** 通常低于
vector
的扩容冗余。
场景例子 :实现 "实时日志系统"------ 需要频繁在日志中间插入紧急记录、删除过期记录。用list
处理 10 万条日志的中间增删,耗时仅为**vector
**的 1/500(数据量越大,差距越明显)。
3. 怎么用 list?(核心接口与限制)
list的接口设计完全贴合链表特性,重点支持 "双向操作",但不支持**vector
**的 "随机访问"。下面分 3 类讲解核心接口,附带代码示例和注意事项。
(1)增删操作:高效的双向增删(O (1))
list
的增删接口是其核心优势,所有接口的时间复杂度均为O(1)
(定位插入位置的时间除外)。
接口 | 功能 | 代码示例 | 注意事项 |
---|---|---|---|
push_front(val) |
在头部插入元素 | list<int> l; l.push_front(1); |
直接修改_head 和首节点的指针 |
push_back(val) |
在尾部插入元素 | l.push_back(2); |
双向循环链表中,直接定位_head->prev |
insert(pos, val) |
在迭代器pos 位置插入元素 |
auto pos = l.begin(); l.insert(pos, 3); |
需先定位pos (O (n)),插入本身 O (1) |
erase(pos) |
删除迭代器pos 指向的元素 |
l.erase(pos); |
删除后pos 失效,其他迭代器有效 |
pop_front() |
删除头部元素 | l.pop_front(); |
需判断链表是否为空 |
pop_back() |
删除尾部元素 | l.pop_back(); |
同上 |
完整增删示例代码:
cpp
#include <list>
#include <iostream>
using namespace std;
int main() {
list<int> l;
// 1. 头部/尾部插入
l.push_front(1); // l: [1]
l.push_back(2); // l: [1, 2]
l.push_front(0); // l: [0, 1, 2]
// 2. 中间插入:定位到1的位置
auto pos = l.begin();
++pos; // pos指向1
l.insert(pos, 5); // l: [0, 5, 1, 2]
// 3. 删除元素:删除5
pos = l.begin();
++pos; // pos指向5
l.erase(pos); // l: [0, 1, 2]
// 4. 头部/尾部删除
l.pop_front(); // l: [1, 2]
l.pop_back(); // l: [1]
// 打印结果:1
for (auto num : l) {
cout << num << " ";
}
return 0;
}
(2)遍历操作:只能用迭代器(不支持 [])
因**list
** 内存不连续,无法通过 "地址偏移" 快速访问元素,所以不支持[]
和at()
,只能用迭代器或范围 for 遍历。
遍历方式 | 代码示例 | 适用场景 |
---|---|---|
正向迭代器 | for (auto it = l.begin(); it != l.end(); ++it) { ... } |
正向遍历所有元素 |
反向迭代器 | for (auto it = l.rbegin(); it != l.rend(); ++it) { ... } |
反向遍历(从尾到头) |
范围 for(C++11+) | for (auto num : l) { ... } |
无需修改元素,简洁遍历 |
const 迭代器 |
for (auto it = l.cbegin(); it != l.cend(); ++it) { ... } |
只读遍历,避免修改元素 |
遍历示例代码:
cpp
list<int> l = {1, 3, 2};
// 1. 正向迭代器(可读可写)
for (auto it = l.begin(); it != l.end(); ++it) {
*it *= 2; // 修改元素:l变为[2, 6, 4]
}
// 2. 反向迭代器(遍历结果:4, 6, 2)
for (auto it = l.rbegin(); it != l.rend(); ++it) {
cout << *it << " ";
}
// 3. const迭代器(只读,无法修改)
for (auto it = l.cbegin(); it != l.cend(); ++it) {
// *it = 10; // 编译报错:const迭代器不可修改
cout << *it << " ";
}
错误提醒 :list
的迭代器不支持 "算术操作"(如it += 3
),只能**++
/** --,若需跳转到第 n 个元素,需循环**++it**n 次:
cpp
// 正确:跳转到第3个元素(索引2)
list<int>::iterator it = l.begin();
for (int i = 0; i < 2; ++i) {
++it;
}
// 错误:不支持随机访问,编译报错
// it += 2;
(3)其他常用接口(O (1) 或 O (n))
接口 | 功能 | 时间复杂度 | 代码示例 |
---|---|---|---|
size() |
返回元素个数 | O(1) | cout << l.size(); |
empty() |
判断是否为空 | O(1) | if (l.empty()) { ... } |
clear() |
清空所有元素(保留头节点) | O(n) | l.clear(); |
swap(list& other) |
交换两个 list 的内容 | O(1) | l1.swap(l2); |
front() |
获取头部元素(引用) | O(1) | cout << l.front(); |
back() |
获取尾部元素(引用) | O(1) | cout << l.back(); |
注意 :clear()
会删除所有数据节点,但保留头节点,size()
变为 0,后续仍可正常**push_back
**。
(4)构造函数
| 构造函数 | 接口说明 | 代码示例 |
| list (size_type n, const value_type& val = value_type()) | 构造的list中包含n个值为val的元素 | list<int> lt(10,1);
|
| list() | 构造空的list | list<int> lt ;
|
| list (const list& x) | 拷贝构造函数 | list<int> lt(l);
|
list (InputIterator first, InputIterator last) | 用[first, last)区间中的元素构造list | list<int>lt(v.begin(),v.end()) |
---|
4. 和 vector 比差在哪?(核心差异对比)
list和vector
是 STL 中最常用的两个序列容器,90% 的场景需要二选一。两者的差异完全源于 "非连续内存 " vs "连续内存",下表从 8 个维度做详细对比:
对比维度 | list(双向链表) | vector(动态数组) | 优势方 |
---|---|---|---|
内存分布 | 节点零散分布,靠指针连接 | 连续一块内存 | vector(缓存友好) |
随机访问 | 不支持(需遍历,O (n)) | 支持([]/at (),O (1)) | vector |
头部增删 | O (1)(改指针) | O (n)(移动所有元素) | list |
中间增删 | O (1)(改指针,需定位 pos) | O (n)(移动后续元素) | list |
尾部增删 | O (1)(改指针) | O (1) amortized(扩容时 O (n)) | 持平(vector 扩容后更优) |
内存利用率 | 高(无扩容浪费) | 低(可能有容量 > 大小的浪费) | list |
迭代器稳定性 | 仅删除节点的迭代器失效 | 扩容 / 中间删除导致多迭代器失效 | list |
指针 / 内存开销 | 高(每个节点 2 个指针) | 低(仅存储数据) | vector |
5. 用 list 有什么坑?(边界与局限)
list
不是 "万能容器",它的短板同样明显,踩坑往往是因为忽略了这些局限:
(1)坑 1:随机访问效率极低,别用它做 "数组"
若需要频繁通过 "索引" 访问元素 (如第i个元素
),**list
**的效率会让你崩溃:
vector
访问第 1000 个元素:v[
999]
(O(1));- **
list
**访问第 1000 个元素:需从头部迭代 999 次(O (n)),数据量越大,差距越悬殊
错误场景 :用list
存储矩阵数据,频繁通过索引访问元素 ------ 程序运行速度会比vector
慢 100 倍以上。
(2)坑 2:内存碎片化与指针开销
每个**list
** 节点除了存储数据,还要存两个指针(prev
和**next
**):
- 64 位系统中,每个指针占 8 字节,若存储**
int
**(4 字节),指针开销(16 字节)是数据本身的 4 倍;- 节点零散分布会导致 "内存碎片化"------ 系统内存被分割成大量小块,后续申请大块内存时可能失败。
(3)坑 3:迭代器不支持 "算术操作",遍历易出错
用**vector
** 的迭代器习惯操作list
迭代器:
cpp
list<int> l = {1,2,3,4};
auto it = l.begin();
// it += 2; // 编译报错:list迭代器不支持+=
// 正确做法:循环++
for (int i = 0; i < 2; ++i) {
++it;
}
cout << *it; // 输出3
(4)坑 4:sort () 效率不如 vector
list有自己的**sort()
** 成员函数(因**std::sort
** 需要随机访问迭代器),但效率不如**vector
** 的**std::sort()
**:
- 两者时间复杂度均为**
O(n log n)
** ,但**list::sort()
**的常数项更大(链表节点跳转需频繁访问指针,缓存命中率低);- 测试:对 100 万个
int
排序,vector
的**std::sort()
** 耗时约 10ms,list::sort()
耗时约25ms。