1. list 基本概念
std::list是 STL 提供的双向循环链表容器 ,底层内存不是连续的,由一个个独立的节点通过前驱、后继指针链接而成,并且 STL 的 list 采用带头哨兵结点的双向循环链表。
vector 是连续的动态数组,list 和 vector 是互补的容器。 list 核心特性:
- 不支持随机访问,没有
[]下标运算符,不能list[2]访问元素;访问中间元素必须迭代器一步步遍历。 - 在已经拿到迭代器的前提下,任意位置插入、删除是 O (1),只需要修改指针指向,不需要挪动数据。
- 内存碎片化,每个节点在堆上单独开辟,CPU 缓存命中率差,遍历速度比 vector 慢很多。
- 迭代器失效规则:插入元素时所有迭代器都不会失效 ;erase 删除某个节点时,只有被删除节点对应的迭代器失效,其余迭代器依旧有效。vector 插入删除会大片失效迭代器,这是 list 很大的优势。
- list 自带成员方法:
sort()、merge()、reverse()、unique(),不要使用 std 算法库的 sort,std::sort 要求随机访问迭代器,list 迭代器不满足,只能调用自身的 sort 成员函数。
vector vs list 对比
| 特性 | vector | list |
|---|---|---|
| 内存 | 连续数组 | 离散双向链表节点 |
| 随机访问 | 支持 O (1) | 不支持 |
| 头部 / 中间插入删除 | O (n),数据搬移 | O (1)(已有迭代器) |
| 迭代器失效 | 插入 / 删除极易大量失效 | 插入全部有效;仅 erase 的节点迭代器失效 |
| 缓存效率 | 高,内存连续 | 低,节点分散堆内存 |
| sort | 使用 std::sort | 只能调用成员函数 sort () |
2. list 常用接口
#include <list>
using namespace std;
void test_list_api()
{
list<int> lt;
//头插、尾插
lt.push_back(10);
lt.push_back(20);
lt.push_front(5);
//头删、尾删
lt.pop_back();
lt.pop_front();
//获取头尾元素
int front_val = lt.front();
int back_val = lt.back();
//大小判空
lt.size();
lt.empty();
//任意位置插入,pos是迭代器
auto pos = lt.begin();
lt.insert(pos, 100); //在pos前面插入元素
//删除迭代器指向节点,返回下一个有效迭代器
lt.erase(pos);
//链表专属成员函数
lt.reverse(); //反转链表
lt.sort(); //链表排序
lt.unique(); //去重(需要元素有序)
list<int> lt2{1,3,5};
lt.merge(lt2); //合并两个有序链表,lt2会变空
//遍历,只能迭代器/范围for
for(auto it = lt.begin(); it != lt.end(); ++it)
{
cout << *it << " ";
}
for(auto x : lt)
{
cout << x << " ";
}
}
注意:list 的
end()迭代器指向哨兵头结点 ,不是最后一个有效节点;begin 指向第一个有效节点。因为是循环链表,--lt.begin()等价于lt.end()。
3. list 底层节点与迭代器原理
list 的节点,保存数据、前驱指针_prev、后继指针_next。 因为内存不连续,原生指针不能作为迭代器。vector 迭代器本质就是T*原生指针;list 迭代器是一个类对象,封装节点指针,重载 ++、--、*、->、!=、== 运算符,模拟指针行为。
迭代器分三类:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器。 list 迭代器属于双向迭代器 :支持++it、--it,但是不支持it + n、it - n。
4. list 模拟实现
模拟实现 STL 标准:带头结点双向循环链表,实现核心接口:构造、析构、push_back、push_front、pop_back、pop_front、insert、erase、begin、end、size、empty,实现迭代器类。
#include <iostream>
using namespace std;
//链表节点
template<class T>
struct ListNode
{
T _data;
ListNode<T>* _prev;
ListNode<T>* _next;
ListNode(const T& val = T())
:_data(val), _prev(nullptr), _next(nullptr)
{}
};
//list迭代器类
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;
ListIterator(Node* node)
:_node(node)
{}
//重载*,返回引用
Ref operator*()
{
return _node->_data;
}
//重载->
Ptr operator->()
{
return &(_node->_data);
}
//前置++
Self& operator++()
{
_node = _node->_next;
return *this;
}
//后置++
Self operator++(int)
{
Self temp = *this;
_node = _node->_next;
return temp;
}
//前置--
Self& operator--()
{
_node = _node->_prev;
return *this;
}
//后置--
Self operator--(int)
{
Self temp = *this;
_node = _node->_prev;
return temp;
}
bool operator!=(const Self& it) const
{
return _node != it._node;
}
bool operator==(const Self& it) const
{
return _node == it._node;
}
};
//模拟list容器
template<class T>
class List
{
public:
typedef ListNode<T> Node;
//普通迭代器、const迭代器
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
private:
Node* _head; //哨兵头结点
//创建哨兵头结点
void CreateHead()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
public:
//构造函数
List()
{
CreateHead();
}
//拷贝构造
List(const List<T>& lt)
{
CreateHead();
for(auto& x : lt)
{
push_back(x);
}
}
//赋值运算符重载
List<T>& operator=(List<T> lt)
{
swap(_head, lt._head);
return *this;
}
//析构
~List()
{
clear();
delete _head;
_head = nullptr;
}
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);
}
bool empty() const
{
return _head->_next == _head;
}
size_t size() const
{
size_t cnt = 0;
auto it = begin();
while(it != end())
{
cnt++;
++it;
}
return cnt;
}
//在pos迭代器之前插入val,返回新元素迭代器
iterator insert(iterator pos, const T& val)
{
Node* cur = pos._node;
Node* newNode = new Node(val);
Node* prevNode = cur->_prev;
prevNode->_next = newNode;
newNode->_prev = prevNode;
newNode->_next = cur;
cur->_prev = newNode;
return iterator(newNode);
}
//尾插
void push_back(const T& val)
{
insert(end(), val);
}
//头插
void push_front(const T& val)
{
insert(begin(), val);
}
//删除pos位置节点,返回下一个有效迭代器
iterator erase(iterator pos)
{
Node* delNode = pos._node;
Node* prevNode = delNode->_prev;
Node* nextNode = delNode->_next;
prevNode->_next = nextNode;
nextNode->_prev = prevNode;
delete delNode;
return iterator(nextNode);
}
//尾删
void pop_back()
{
erase(--end());
}
//头删
void pop_front()
{
erase(begin());
}
//清空所有有效节点,哨兵结点保留
void clear()
{
iterator it = begin();
while(it != end())
{
it = erase(it);
}
}
T& front()
{
return *begin();
}
T& back()
{
return *(--end());
}
};
//测试模拟实现
void test_mylist()
{
List<int> mylt;
mylt.push_back(1);
mylt.push_back(2);
mylt.push_front(0);
for(auto x : mylt)
{
cout << x << " ";
}
cout << endl;
mylt.pop_back();
mylt.pop_front();
for(auto x : mylt)
{
cout << x << " ";
}
}
int main()
{
test_mylist();
return 0;
}
5. 关键细节解析
- 哨兵头结点的好处 :空链表的时候
_head->_next = _head,begin() = end(),插入删除不需要单独判断链表是否为空,代码逻辑统一。 - 迭代器模板参数:
ListIterator<T,Ref,Ptr>,Ref 代表引用类型,Ptr 代表指针;一份迭代器类模板,实例化出普通迭代器和 const 迭代器,不用写两套代码。 - insert:list 的 insert 不会失效任何迭代器。插入节点,原有节点指针只是修改指向,原有迭代器封装的节点地址不变。
- erase:只有被删掉节点的迭代器失效,其他迭代器保持有效。erase 返回下一个有效迭代器,遍历删除元素标准写法:
it = lt.erase(it);,不要lt.erase(it); ++it;,会迭代器失效。 - push_back、push_front 复用 insert,不需要重复写指针操作逻辑。
6. list 使用坑点
- ❌不要用
std::sort(lt.begin(), lt.end()),std::sort 要求随机访问迭代器,list 只有双向迭代器,编译报错;要用lt.sort()成员函数。 - ❌不能随机访问,不支持
it + 3,双向迭代器只允许++it、--it。 - list 每个节点 new,频繁增删小对象会产生大量内存碎片。
- unique 去重只会去掉相邻重复元素,调用 unique 之前必须先 sort 排序。
- merge 合并链表,要求两个链表都是有序;merge 之后被合并的链表会被置空。
7. vector 与 list 选型
- 如果需要大量随机访问,少量头部中间增删 → 使用 vector。
- 如果大量在中间、头部插入删除,不在乎遍历速度 → 使用 list。
- 实际开发中 vector 使用频率远高于 list,list 缓存失效带来的性能损耗往往比数组搬移更大。