一、List 容器概述
1.1底层结构与特性
- 数据结构:双向循环链表(带哨兵位头结点),每个节点包含前驱指针、后继指针和数据域。
- 核心优势:
- 高效插入 / 删除:任意位置操作时间复杂度为 O (1),无需移动元素。
- 灵活迭代器:支持双向遍历(正向迭代器
begin/end
、反向迭代器rbegin/rend
)。
- 局限性:
- 不支持随机访问(如
operator[]
),元素访问时间复杂度为 O (N)。 - 内存碎片化问题:动态节点分配可能导致缓存利用率低。
- 不支持随机访问(如
典型应用场景
- 频繁插入 / 删除操作的场景(如链表结构的动态数据管理)。
- 无需随机访问,注重数据动态调整的场景(如任务队列、事件链表)。
1.2 与数组(vector)的区别
特性 | list(链表) | vector(数组) |
---|---|---|
内存分布 | 非连续(动态节点) | 连续(一块内存) |
随机访问 | 慢(需逐个遍历) | 快(下标直接访问) |
插入 / 删除效率 | 快(仅修改指针) | 慢(需移动元素) |
典型用法 | 队列、链表结构 | 数组运算、随机访问场景 |
二、List 接口详解与实践
2.1 构造函数
构造方式 | 代码示例 | 说明 |
---|---|---|
默认构造 | list<int> l1; |
空链表 |
初始化数量与值 | list<int> l1(5, 10); |
5 个值为 10 的元素 |
区间构造 | list<int> l1(arr, arr+5); |
用数组 arr 的前 5 个元素初始化 |
拷贝构造 | list<int> l2(l1); |
复制已有链表 l1 |
2.1.1 头文件与命名空间
#include <list> // list 头文件
using namespace std; // 使用 std 命名空间
2.1.2 创建 list
(1)空 list
list<int> numbers; // 空链表,存储 int 类型数据
(2)初始化指定元素
- 指定数量和值:
list<int> l1(5, 10); // 5个元素,每个值都是 10
- 用数组 / 其他容器初始化:
int arr[] = {1, 2, 3}; list<int> l2(arr, arr + 3); // 用数组前3个元素初始化
- 拷贝其他 list:
list<int> l3(l2); // 复制 l2 的所有元素
2.2 迭代器操作
- 正向迭代器:
begin()
指向首元素,end()
指向尾后节点(哨兵位)。 - 反向迭代器:
rbegin()
指向尾元素,rend()
指向头前节点(哨兵位)。 - 特性:
- 正向迭代器
++
向后移动,反向迭代器++
向前移动。 - 支持
!=
、==
比较。
- 正向迭代器
代码演示(遍历元素):
list<int> l = {1, 3, 5, 7, 9};
// 正向遍历
for (auto it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 输出:1 3 5 7 9
}
// 反向遍历
for (auto rit = l.rbegin(); rit != l.rend(); ++rit) {
cout << *rit << " "; // 输出:9 7 5 3 1
}
2.3 容量与元素访问
接口 | 功能 | 返回值 |
---|---|---|
empty() |
判断链表是否为空 | bool |
size() |
返回有效节点数 | size_type |
front() |
返回首元素引用 | T& |
back() |
返回尾元素引用 | T& |
2.3.1检查空链表与获取长度
list<int> l;
if (l.empty()) { // 判断是否为空
cout << "链表为空!";
}
l.push_back(1);
cout << "元素个数:" << l.size(); // 输出:1
2.3.2获取首尾元素
list<int> l = {1, 3, 5};
cout << "首元素:" << l.front(); // 输出:1
cout << "尾元素:" << l.back(); // 输出:5
注意:避免在空链表上调用 front()
/back()
,可能引发未定义行为。
2.4 增删改操作
接口 | 功能描述 | 时间复杂度 |
---|---|---|
push_front(val) |
头部插入元素 | O(1) |
push_back(val) |
尾部插入元素 | O(1) |
pop_front() |
头部删除元素 | O(1) |
pop_back() |
尾部删除元素 | O(1) |
insert(pos, val) |
在 pos 位置插入元素 |
O(1) |
erase(pos) |
删除 pos 位置的元素 |
O(1) |
clear() |
清空所有元素 | O(N) |
关键细节:
- 插入操作:
insert
返回新插入元素的迭代器,可用于连续插入。 - 删除操作:删除后,指向被删节点的迭代器失效,其他迭代器不受影响。
2.4.1 向 list 中添加元素
(1)头部插入(快!O (1))
list<int> l;
l.push_front(1); // 头部插入 1 → list: [1]
l.push_front(0); // 头部插入 0 → list: [0, 1]
(2)尾部插入(快!O (1))
l.push_back(2); // 尾部插入 2 → list: [0, 1, 2]
(3)任意位置插入(需迭代器定位)
auto it = l.begin(); // 指向首元素(0)的迭代器
l.insert(it, 99); // 在 it 位置插入 99 → list: [99, 0, 1, 2]
2.4.2. 从 list 中删除元素
(1)头部删除(快!O (1))
l.pop_front(); // 删除头部元素 99 → list: [0, 1, 2]
(2)尾部删除(快!O (1))
l.pop_back(); // 删除尾部元素 2 → list: [0, 1]
(3)删除指定位置元素
auto it = l.begin(); // it 指向 0
++it; // it 指向 1
l.erase(it); // 删除 1 → list: [0]
(4)删除指定值的元素(需遍历查找)
list<int> l = {1, 2, 3, 2, 4};
l.remove(2); // 删除所有值为 2 的元素 → list: [1, 3, 4]
代码演示(删除偶数元素):
list<int> l = {1, 2, 3, 4, 5};
auto it = l.begin();
while (it != l.end()) {
if (*it % 2 == 0) {
it = l.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
// 结果:l = {1, 3, 5}
2.5 迭代器失效问题
- 插入场景:不会导致迭代器失效(链表节点独立,插入不影响其他节点指针)。
- 删除场景:
- 被删除节点的迭代器失效。
- 正确做法:使用
erase(it++)
或it = erase(it)
更新迭代器。
错误示例:
- 场景:删除元素后,指向被删节点的迭代器会失效,需及时更新。
- 错误代码:
list<int> l = {1, 2, 3}; auto it = l.begin(); while (it != l.end()) { if (*it == 2) { l.erase(it); // 错误!it 失效,下次循环非法访问 } ++it; // 这里会访问失效的迭代器 }
- 正确代码:
while (it != l.end()) { if (*it == 2) { it = l.erase(it); // 用 erase 返回的下一个迭代器更新 it } else { ++it; } }
三、List 模拟实现关键点
3.1 节点结构定义
template <class T>
struct ListNode {
T val;
ListNode* prev;
ListNode* next;
ListNode(const T& x) : val(x), prev(nullptr), next(nullptr) {}
};
3.2 正向迭代器实现
- 核心成员:封装节点指针
ListNode<T>* node
。 - 操作重载:
operator*()
:返回节点值(return node->data;
)。operator++()
:前置递增,指向next
节点(node = node->next;
)。operator--()
:前置递减,指向prev
节点(node = node->prev;
)。
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T,Ref,ptr> Self;
Node* _pnode;
list_iterator(Node* val)
:_pnode(val)
{ }
Ref operator*()
{
return _pnode->_data;
}
ptr operator->()
{
return &_pnode->_data;
}
Self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
Self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
Self operator--(int)
{
Self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
bool operator!=(const Self& s)
{
return _pnode != s._pnode;
}
bool operator==(const Self& s)
{
return _pnode == s._pnode;
}
};
四、List 与 Vector 对比
特性 | Vector | List | 关键差异原因 |
---|---|---|---|
底层结构 | 连续内存(动态数组) | 非连续内存(双向链表) | 内存布局决定访问与修改效率 |
随机访问 | O (1)(支持 [] ) |
O (N)(需遍历链表) | 数组下标 vs 指针遍历 |
插入 / 删除 | 平均 O (N)(移动元素) | O (1)(修改指针) | 数组元素搬移 vs 链表指针操作 |
内存效率 | 高(缓存友好) | 低(节点指针开销) | 连续内存 vs 碎片化内存 |
迭代器类型 | 原生指针(T* ) |
封装迭代器(含指针逻辑) | 链表需要封装指针操作 |
迭代器失效 | 插入可能全量失效(扩容) | 仅删除节点失效 | 数组扩容导致指针失效 vs 链表节点独立 |
典型场景 | 数据频繁访问(如数组运算) | 数据频繁增删(如消息队列) | 根据操作模式选择容器 |
五、常见错误
5.1. 不支持随机访问(与数组的区别)
- 错误用法:
list<int> l = {1, 2, 3}; cout << l[1]; // 错误!list 没有 [] 运算符
- 正确做法:用迭代器遍历或
front()
/back()
获取首尾元素。
5.2. 空链表操作风险
- 错误用法:
list<int> l; cout << l.front(); // 空链表调用 front(),程序可能崩溃
- 正确做法:先检查
empty()
:if (!l.empty()) { cout << l.front(); }
六、总结
list
是 STL 中基于双向循环链表的容器,适合频繁增删、无需随机访问的场景。其核心优势在于 O (1) 时间复杂度的插入 / 删除操作,以及双向遍历能力,但代价是较高的内存开销和较低的缓存利用率。学习 list
时,需重点掌握:
- 迭代器的双向操作与失效规则(尤其是删除后的迭代器更新)。
- 与
vector
的对比,根据实际需求选择容器。 - 模拟实现时双向链表与迭代器封装的逻辑。
附录
模拟实现
//stl_list.h
#pragma once
#include<iostream>
#include<assert.h>
namespace my_list
{
template<class T>
struct list_node
{
list_node* _prev;
list_node* _next;
T _data;
list_node(const T& val=T())
:_data(val)
,_prev(nullptr)
,_next(nullptr)
{ }
};
template<class T,class Ref,class ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T,Ref,ptr> Self;
Node* _pnode;
list_iterator(Node* val)
:_pnode(val)
{ }
Ref operator*()
{
return _pnode->_data;
}
ptr operator->()
{
return &_pnode->_data;
}
Self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
Self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
Self operator--(int)
{
Self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
bool operator!=(const Self& s)
{
return _pnode != s._pnode;
}
bool operator==(const Self& s)
{
return _pnode == s._pnode;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef list_iterator<T,T&,T*> iterator;
typedef list_iterator<T,const T&,const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
const_iterator begin()const
{
return const_iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator end()const
{
return const_iterator(_head);
}
void empty_init()
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
_size = 0;
}
list()
{
empty_init();
}
list(std::initializer_list<T> il)
{
empty_init();
for (auto e : il)
{
push_back(e);
}
}
list(const list<T>& ll)
{
empty_init();
for (auto e : ll)
{
push_back(e);
}
}
list<T>& operator=(list<T> ll)
{
swap(ll);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
size_t size()
{
return _size;
}
void swap(list<T>& ll)
{
std::swap(_head, ll._head);
std::swap(_size, ll._size);
}
void push_back(const T& val)
{
/*Node* tmp = new Node(val);
tmp->_prev = _head->_prev;
tmp->_next = _head;
_head->_prev->_next = tmp;
_head->_prev = tmp;
_size++;*/
insert(end(), val);
}
void push_head(const T& val)
{
insert(begin(), val);
}
void pop_back()
{
erase(--end());
}
void pop_head()
{
erase(begin());
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
void insert(iterator pos, const T& val)
{
Node* newnode = new Node(val);
Node* tmp = pos._pnode;
newnode->_next = tmp;
newnode->_prev = tmp->_prev;
tmp->_prev->_next = newnode;
tmp->_prev = newnode;
++_size;
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._pnode;
Node* tmp = cur->_next;
cur->_prev->_next = cur->_next;
cur->_next->_prev = cur->_prev;
delete cur;
_size--;
return tmp;
}
private:
Node* _head;
size_t _size;
};
}