目录
①头部插入(push_front)与尾部插入(push_back)
①头部删除(pop_front)与尾部删除(pop_back)
在C++STL的序列式容器中,list是一个独特的存在------它是基于双向链表实现,与vector的连续内存空间形成鲜明对比。
🔁一、list的底层结构------双向链表
list 的核心是双向链表 ,这意味着它的元素在内存中非连续存储 ,并且通过指针相互连接。
1️⃣节点结构
list 的每个元素都被封装在一个 "节点(node)" 中,节点间通过指针链接。GCC 标准库中节点的核心定义如下(简化版):
cpp
template <typename T>
struct _List_node {
typedef _List_node<T>* pointer;
pointer _M_next; // 指向后一个节点的指针
pointer _M_prev; // 指向前一个节点的指针
T _M_data; // 存储的元素数据
};
- 每个节点都包含三个部分:前驱指针(
_M_pprev)、后继指针(_M_next)、元素数据(_M_data) - 链表的 "头节点" 通常是一个哨兵节点(sentinel) ,不存储实际数据,仅用于简化边界操作(如头插、尾插的逻辑统一)
2️⃣链表的布局

如果链表为空,那么头节点的 prev 和 next 都指向自己,形成一个环。
- 哨兵节点的存在让链表的 "空状态" 和 "非空状态" 处理逻辑一致(避免判断
nullptr) - 双向循环特性使头插、尾插、头删、尾删的操作复杂度均为 O (1)
3️⃣list的begin()和end()
list 容器本身持有指向哨兵节点的指针,以及分配器(管理节点内存):
cpp
template <typename T, typename Alloc = allocator<T>>
class list {
private:
_List_node<T>* _M_node; // 指向哨兵节点的指针
allocator_type _M_get_allocator() const; // 节点内存分配器
public:
// 迭代器相关(见下文)
iterator begin() { return _M_node->_M_next; }
iterator end() { return _M_node; } // end() 指向哨兵节点
// ... 其他接口
};
- begin() 返回第一个元素的迭代器(哨兵节点的 _M_next)。
- end() 返回哨兵节点的迭代器。
🔎二、list迭代器:双向访问的实现
list 的迭代器是双向迭代器 ,支持 ++(向后移动)和 --(向前移动),但不支持随机访问(就比如 it + 5 是不允许的)。
1️⃣迭代器的底层实现
迭代器本质是封装了节点指针的对象,通过重载 *、->、++、-- 等运算符实现元素访问和移动:
cpp
template <typename T>
struct _List_iterator {
typedef _List_node<T>* pointer;
pointer _M_node; // 指向当前节点的指针
// 解引用:获取元素数据
reference operator*() const { return _M_node->_M_data; }
// 箭头运算符:访问元素成员
pointer operator->() const { return &(operator*()); }
// 前置++:移动到下一个节点
_List_iterator& operator++() {
_M_node = _M_node->_M_next;
return *this;
}
// 前置--:移动到上一个节点
_List_iterator& operator--() {
_M_node = _M_node->_M_prev;
return *this;
}
};
- 迭代器的
++和--操作本质是修改节点指针(_M_next/_M_prev),时间复杂度 O (1)。 - 不支持随机访问的原因:链表元素并不是连续的,无法通过指针偏移直接定位(比如
it + n需要遍历 n 个节点,复杂度 O (n))。
2️⃣迭代器的失效规则
与 vector 扩容导致大规模迭代器失效不同,list 的迭代器失效规则更简单:
- 删除元素时:仅指向被删除节点的迭代器失效,其他迭代器(包括被删除节点的前驱、后继)仍有效。
- 插入元素时:所有迭代器均有效(因为插入不会影响已有节点的指针)。
这是链表结构的显著优势 ------ 插入 / 删除操作对其他节点无影响,仅需调整相邻节点的指针。
📈三、list的操作与性能
1️⃣元素插入(O(1)复杂度优势)
list 支持在任意位置插入元素,且时间复杂度均为 O (1)(只需调整指针,无需移动元素)。
①头部插入(push_front)与尾部插入(push_back)
cpp
list<int> list;
list.push_back(10);
list.push_front(9);
- 头插:
- 仅仅需要调整哨兵节点的
_M_next和原来头结点的指针
- 尾插:
- 建新节点,数据为 10
- 将新节点的 _M_prev 指向原尾节点
- 将原尾节点的 _M_next 指向新节点
- 将哨兵节点的 _M_prev 指向新节点
②任意位置插入(insert)
cpp
auto it = list.begin(); // 指向第一个元素
list.insert(it, 30); // 在 it 前插入 30
- 底层逻辑:
- 创建新节点,数据为 30
- 新节点的 _M_prev = it 节点的 _M_prev(就是新节点的
_M_prev指向it节点的_M_prev所指向的节点)- 新节点的 _M_next = it 节点
- 调整 it 节点的前一个结点的 _M_next 指向新节点
- 调整 it 节点的 _M_prev 指向新节点
- 无论插入位置在哪里(头部、中间、尾部),均只需修改 4 个指针,复杂度 O (1)(前提是已获取插入位置的迭代器)。
2️⃣元素删除(O(1)复杂度优势)
list 的删除操作同样只需调整指针,无需移动元素,时间复杂度 O (1)。
①头部删除(pop_front)与尾部删除(pop_back)
cpp
list.pop_front();
list.pop_back();
- 尾删底层逻辑 :
- 获取原尾节点(哨兵节点的
_M_prev)- 将哨兵节点的
_M_prev指向原尾节点的_M_prev(更新尾节点)- 将新尾节点的
_M_next指向哨兵节点- 销毁原尾节点的元素,释放节点内存
②任意位置删除(erase)
cpp
auto it = list.begin();
it = list.erase(it); // 删除 it 指向的元素,返回下一个元素的迭代器
- 底层逻辑:
- 获取待删除节点的前驱(p = it->_M_prev)和后继(n = it->_M_next)
- 调整 p->_M_next = n 和 n->_M_prev = p(跳过待删除节点)
- 销毁待删除节点的元素,释放节点内存
- 返回指向 n 的迭代器(避免原迭代器失效)
3️⃣元素访问的劣势
list不支持operator[]或at()和随机访问,访问第n个元素必须从头部或者是尾部开始遍历,时间复杂度O(n)
这是list最显著的劣势------不适合需要频繁随机访问的场景
4️⃣其他操作
- clear():销毁所有元素,释放节点内存,但保留哨兵节点,
size()变为 0。 - swap():交换两个
list的哨兵节点指针,O (1) 复杂度(与vector相同)。 - splice():将一个
list的部分或全部元素转移到另一个list,仅调整指针,O (1) 复杂度(list特有的高效操作)。
⚔️四、list和vector的核心差异
|-------------|---------------------|-------------------------|
| 特性 | list(双向链表) | vector(动态数组) |
| 内存布局 | 非连续,节点通过指针链接 | 连续内存块 |
| 随机访问 | 不支持(O (n)) | 支持(O (1)) |
| 插入 / 删除(中间) | O (1)(仅调整指针) | O (n)(需移动元素) |
| 插入 / 删除(尾部) | O(1) | O (1)(无扩容时)/ O (n)(扩容时) |
| 迭代器失效规则 | 仅被删除元素的迭代器失效 | 扩容时所有失效,插入 / 删除中间时部分失效 |
| 内存利用率 | 有额外指针开销(每个节点 2 个指针) | 连续内存,无指针开销,但可能有预留空间浪费 |
| 缓存友好性 | 差(元素分散,缓存命中率低) | 好(连续内存,缓存命中率高) |
- 若需频繁随机访问 (如
v[i]),选vector- 若需频繁在中间插入 / 删除 ,选
list- 若元素体积大 (如大对象),
list的插入 / 删除优势更明显(避免vector的大量拷贝)- 若操作以尾部增删 为主,
vector更优(缓存友好,无指针开销)
⚠️五、list使用需注意的事项
1️⃣避免频繁的随机访问
比如,遍历list的时候使用迭代器来移动(++it)是O(n),但是通过下标访问每个元素会变成O(n2)
cpp
// 模拟下标访问 - O(n²)
for (size_t i = 0; i < lst.size(); ++i)
{
auto it = lst.begin();
std::advance(it, i); // 每次从头开始移动 i 步
std::cout << *it << " ";
}
2️⃣迭代器失效的特殊情况
删除元素后,仅被删除的迭代器失效 ,其他迭代器仍可以使用
cpp
list<int> list = {1,2,3,4};
auto it = list.begin();
++it; // 指向 2
list.erase(l.begin()); // 删除 1,it 仍指向 2(有效)
插入元素后,所有迭代器都有效(包括插入位置的迭代器)
cpp
auto it = list.begin();
list.insert(it, 10); // 插入后 it 仍指向原元素(有效)
3️⃣对splice()的了解
list 的 splice() 是独特的高效操作,可在 O (1) 时间内转移元素(无需拷贝,仅调整指针),适合批量元素移动:
cpp
list<int> l1 = {1,2,3};
list<int> l2 = {4,5,6};
// 将 l2 的所有元素移到 l1 末尾
l1.splice(l1.end(), l2); // l1 = {1,2,3,4,5,6},l2 为空
📚六、总结
list凭借双向链表的特性,在任意位置插入 / 删除 场景中展现出 O (1) 复杂度的优势,但也因为非连续内存 导致随机访问能力缺失、缓存友好性差。使用时需根据具体场景权衡:频繁中间操作选list,频繁随机访问选vector。