在 C++ 标准模板库(STL)中,list 是一个非常重要的序列式容器。与我们熟知的 vector 不同,它以带头结点的双向循环链表作为底层结构,这使得它在插入和删除操作上拥有得天独厚的优势。
本文将带你系统梳理 list 的核心特性、常用接口、迭代器失效机制,并深入到源码层面,模拟实现一个简化版的 list,最后通过与 vector 的对比,帮助你在实际开发中做出更优的容器选择。
1. list 的介绍及使用
1.1 list 的介绍
• 底层结构:list 是一个带头结点的双向循环链表。
• 核心特点:
支持在任意位置高效地插入和删除操作,时间复杂度为 O(1)。
不支持随机访问,访问中间元素需要从头/尾遍历,时间复杂度为 O(N)。
迭代器失效情况与 vector 不同,插入操作不会导致迭代器失效,删除操作仅使指向被删除节点的迭代器失效。
1.2 list 的使用
1.2.1 list 的构造
|----------------------------------------------------------|--------------------------------|
| 构造函数 (constructor) | 接口说明 |
| list(size_type n, const value_type& val = value_type()) | 构造的 list 中包含 n 个值为 val 的元素 |
| list() | 构造空的 list |
| list(const list& x) | 拷贝构造函数 |
| list(InputIterator first, InputIterator last) | 用 [first, last) 区间中的元素构造 list |
代码示例1
cpp
#include <iostream>
#include <list>
#include <vector>
using namespace std;
void test_list_construct()
{
// 1. 空构造函数 list()
list<int> l1;
cout << "l1 空构造,size: " << l1.size() << endl;
// 2. 构造 n 个值为 val 的元素 list(size_type n, const value_type& val = value_type())
list<int> l2(5, 10); // 5个10
cout << "l2(5,10): ";
for (auto e : l2) cout << e << " ";
cout << endl;
// 3. 拷贝构造函数 list(const list& x)
list<int> l3 = l2;
cout << "l3 拷贝 l2: ";
for (auto e : l3) cout << e << " ";
cout << endl;
// 4. 迭代器区间构造 list(InputIterator first, InputIterator last)
vector<int> v = { 1,2,3,4,5 };
list<int> l4(v.begin(), v.end());
cout << "l4 迭代器区间构造: ";
for (auto e : l4) cout << e << " ";
cout << endl;
}
输出:
cpp
l1 空构造,size: 0
l2(5,10): 10 10 10 10 10
l3 拷贝 l2: 10 10 10 10 10
l4 迭代器区间构造: 1 2 3 4 5
• list():构造空链表
• list(n, val):构造包含 n 个值为 val 的节点
• list(const list& x):拷贝构造,用一个已存在的 list 初始化新 list
• list(first, last):用迭代器区间初始化,可接收其他容器的区间
代码示例2
cpp
#include <iostream>
using namespace std;
struct A
{
public:
// 1. 带缺省值的构造函数(也是默认构造函数)
A(int a1 = 1, int a2 = 1)
:_a1(a1) // 初始化列表:初始化 _a1
,_a2(a2) // 初始化列表:初始化 _a2
{
cout << "A(int a1 = 1, int a2 = 1)" << endl;
}
// 2. 拷贝构造函数 格式:类名(const 类名& 对象名)
// 作用:用一个已存在的对象去初始化另一个同类型的新对象,这里是浅拷贝(默认拷贝构造也是浅拷贝)
A(const A& aa)
:_a1(aa._a1) // 用传入对象 aa 的 _a1 初始化当前对象 _a1
,_a2(aa._a2) // 用传入对象 aa 的 _a2 初始化当前对象 _a2
{
cout << "A(const A& aa)" << endl;
}
// 成员变量
int _a1;
int _a2;
};
void test_A()
{
A a1; // 调用 构造函数
A a2(10, 20); // 调用 构造函数
A a3 = a2; // 调用 拷贝构造
}
运行输出:
cpp
A(int a1 = 1, int a2 = 1)
A(int a1 = 1, int a2 = 1)
A(const A& aa)
核心知识点总结:
-
构造函数:可以带缺省参数;支持初始化列表,效率更高;无返回值,函数名与类名相同
-
拷贝构造函数:是构造函数的重载;参数必须是 const 类名&;用已有对象初始化新对象;不写会自动生成默认浅拷贝拷贝构造
-
struct 与 class:这里 struct 用法和 class 几乎一样;默认访问权限是 public
1.2.2 list iterator 的使用
可以暂时将迭代器理解为一个"指针",指向 list 中的某个节点。
|-------------------|--------------------------------------------------------------------------------------|
| 函数声明 | 接口说明 |
| begin() + end() | begin() 返回第一个元素的迭代器;end() 返回最后一个元素下一个位置的迭代器 |
| rbegin() + rend() | 返回第一个元素的 reverse_iterator,即 end 位置;rend() 返回最后一个元素下一个位置的 reverse_iterator,即 begin 位置 |
注意:
-
begin 与 end 为正向迭代器,对迭代器执行 ++ 操作,迭代器向后移动。
-
rbegin(end) 与 rend(begin) 为反向迭代器,对迭代器执行 ++ 操作,迭代器向前移动。
代码示例:
cpp
#include <iostream>
#include <list>
using namespace std;
void test_list_iterator()
{
list<int> lt = { 1, 2, 3, 4, 5 };
// 1. begin() + end() 正向迭代器
cout << "begin() ~ end() 正向遍历: ";
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 2. rbegin() + rend() 反向迭代器
cout << "rbegin() ~ rend() 反向遍历: ";
list<int>::reverse_iterator rit = lt.rbegin();
while (rit != lt.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
输出:
cpp
begin() ~ end() 正向遍历: 1 2 3 4 5
rbegin() ~ rend() 反向遍历: 5 4 3 2 1
接口说明:
• begin() + end()
begin():返回指向第一个元素的迭代器; end():返回指向最后一个元素的下一个位置的迭代器(左闭右开)
• rbegin() + rend()
rbegin():返回反向第一个元素(原最后一个)的反向迭代器; rend():返回反向最后一个的下一个位置(原第一个的前一个); 对反向迭代器 ++,会从后往前走
1.2.3 list capacity
|-------|----------------------------------|
| 函数声明 | 接口说明 |
| empty | 检测 list 是否为空,是返回 true,否则返回 false |
| size | 返回 list 中有效节点的个数 |
代码示例:
cpp
#include <iostream>
#include <list>
using namespace std;
void test_list_capacity()
{
list<int> lt1; // 空链表
list<int> lt2 = { 10,20,30,40 }; // 4个元素
// 1. empty():是否为空
cout << "lt1.empty():" << lt1.empty() << endl;
cout << "lt2.empty():" << lt2.empty() << endl;
// 2. size():有效节点个数
cout << "lt1.size():" << lt1.size() << endl;
cout << "lt2.size():" << lt2.size() << endl;
}
输出:
cpp
lt1.empty():1
lt2.empty():0
lt1.size():0
lt2.size():4
接口说明:
• empty():检测 list 是否为空,为空返回 true,否则返回 false。
• size():返回 list 中有效节点的个数。
1.2.4 list element access
|-------|----------------------|
| 函数声明 | 接口说明 |
| front | 返回 list 的第一个节点中值的引用 |
| back | 返回 list 的最后一个节点中值的引用 |
代码示例:
cpp
#include <iostream>
#include <list>
using namespace std;
void test_list_front_back()
{
list<int> lt = {10, 20, 30, 40};
// front:第一个元素的引用
cout << "lt.front() = " << lt.front() << endl;
// back:最后一个元素的引用
cout << "lt.back() = " << lt.back() << endl;
// 可以通过引用修改
lt.front() = 1;
lt.back() = 4;
cout << "修改后:";
for (auto e : lt) cout << e << " ";
}
输出:
cpp
lt.front() = 10
lt.back() = 40
修改后:1 20 30 4
接口说明:
• front:返回 list 的第一个节点中值的引用,可读写。
• back:返回 list 的最后一个节点中值的引用,可读写。
1.2.5 list modifiers
|------------|---------------------------------|
| 函数声明 | 接口说明 |
| push_front | 在 list 首元素前插入值为 val 的元素 |
| pop_front | 删除 list 中第一个元素 |
| push_back | 在 list 尾部插入值为 val 的元素 |
| pop_back | 删除 list 中最后一个元素 |
| insert | 在 list position 位置中插入值为 val 的元素 |
| erase | 删除 list position 位置的元素 |
| swap | 交换两个 list 中的元素 |
| clear | 清空 list 中的有效元素 |
代码示例1:遍历
cpp
#include <iostream>
#include <list>
#include <algorithm>
#include <string>
using namespace std;
void test_list1()
{
// 1. 创建list并插入元素
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
// 2. 使用迭代器遍历list
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " "; // 解引用迭代器获取元素值
++it; // 迭代器向后移动
}
cout << endl;
// 输出: 1 2 3 4
// 3. 使用范围for循环遍历list
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
// 输出: 1 2 3 4
// 4. 错误示例:list不支持随机访问迭代器
// it = lt.begin();
// lt.erase(it + 3); // 编译错误!list的迭代器是双向迭代器,不支持operator+操作
// 5. 错误示例:对list使用std::sort
// sort(lt.begin(), lt.end()); // 编译错误!std::sort要求随机访问迭代器
// 正确做法:使用list自带的sort成员函数:lt.sort();
// 6. 对比:std::string支持随机访问,可以使用std::sort
string s("dadawdfadsa");
cout << s << endl; // 输出: dadawdfadsa
sort(s.begin(), s.end()); // 对string排序
cout << s << endl; // 输出: aaaaadddffw
}
核心知识点总结:
-
std::list 的迭代器类型: list 的迭代器是双向迭代器 (Bidirectional Iterator),不是随机访问迭代器。 因此,它支持 ++ 和 -- 操作,但不支持 +、-、[] 等随机访问操作。
-
std::sort 的要求: 标准库函数 std::sort 要求迭代器必须是随机访问迭代器 (Random Access Iterator)。
由于 list 的迭代器不满足此要求,所以不能直接对 list 使用 std::sort。
正确的做法是使用 list 容器自身提供的 lt.sort() 成员函数,它是为链表结构专门优化的排序算法。
- std::string 的特殊性: std::string 的底层是连续数组,其迭代器是随机访问迭代器,因此可以直接使用 std::sort。
代码示例2:push_back 与 emplace_back
cpp
struct A
{
public:
A(int a1 = 1, int a2 = 1)
:_a1(a1)
,_a2(a2)
{
cout << "A(int a1 = 1, int a2 = 1)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
,_a2(aa._a2)
{
cout << "A(const A& aa)" << endl;
}
int _a1;
int _a2;
};
// 测试 list 存储自定义类型
// push_back 与 emplace_back 的区别
void test_list2()
{
// 定义一个存储 A 类型对象的 list
list<A> lt;
// 1. 定义一个 A 对象 aa1,调用【构造函数】
A aa1(1, 1);
// 2. push_back(左值对象)
// 传入已经存在的对象 aa1(左值)
// 会调用【拷贝构造】,把 aa1 拷贝到链表节点里
lt.push_back(aa1);
// 3. push_back(临时对象)
// A(2,2) 是临时对象(右值)
// 先构造临时对象 → 再拷贝/移动到链表节点
lt.push_back(A(2,2));
// 错误写法!
// push_back 只能传 1 个参数(必须是 A 对象)
// 不能直接传(3,3)去构造
// lt.push_back(3, 3);
// 4. emplace_back(左值对象)
// 效果和 push_back(aa1) 一样
// 还是会调用【拷贝构造】
lt.emplace_back(aa1);
// 5. emplace_back(临时对象)
// 先构造临时对象 A(2,2)
// 再移动/拷贝到链表节点
lt.emplace_back(A(2,2));
cout << endl;
// 6. emplace_back(构造参数) ------ 核心重点!
// 直接把(3,3)传给 emplace_back
// emplace_back 会在链表节点的内存里
// **原地直接调用 A 的构造函数**
// 没有拷贝!没有移动!没有临时对象!
lt.emplace_back(3, 3);
}
运行 test_list2() 输出顺序 精确如下:
cpp
A(int a1 = 1, int a2 = 1) // A aa1(1,1);
A(const A& aa) // lt.push_back(aa1);
A(int a1 = 1, int a2 = 1) // A(2,2) 构造临时对象
A(const A& aa) // push_back 拷贝临时对象
A(const A& aa) // lt.emplace_back(aa1);
A(int a1 = 1, int a2 = 1) // A(2,2) 构造临时对象
A(const A& aa) // emplace_back 拷贝临时对象
A(int a1 = 1, int a2 = 1) // lt.emplace_back(3,3); 原地构造!
核心知识点总结
- push_back 的特点: 只能接受 一个参数,必须是 A 对象
• 传左值 → 拷贝构造 传右值 → 先构造临时对象,再拷贝 无法直接传 (3,3) 去构造
- emplace_back 的特点(C++11)
• 模板可变参:template<class... Args> emplace_back(Args&&... args)
• 可以直接传 构造函数需要的参数; 在容器节点原地构造对象; 少一次拷贝 / 少一次移动,效率更高
- 最关键区别
cpp
lt.push_back(A(3,3));
// 构造临时对象 + 拷贝/移动
lt.emplace_back(3,3);
// 直接原地构造,无临时对象、无拷贝
一句话总结: push_back:先造对象,再放进容器; emplace_back:直接在容器里造对象
• 存放自定义类型时,优先用 emplace_back,效率更高
代码示例3:insert、erase、find
cpp
// list 的 insert 插入、erase 删除、find 查找
void test_list3()
{
// 1. 创建list,并尾插6个元素
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
// 遍历打印初始链表
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
// 输出:1 2 3 4 5 6
// 2. 把迭代器向后移动 3 次
// begin() 指向 1
auto it = lt.begin();
int k = 3;
while (k--)
{
++it;
}
// 移动3次后:
// 1→2→3→4 it 最终指向 4
// 3. 在迭代器指向的位置 **前面** 插入 30
lt.insert(it, 30);
// 插入后链表变为:
// 1 2 3 30 4 5 6
// 打印插入后的结果
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
// 输出:1 2 3 30 4 5 6
// 4. 输入一个值,查找并删除
int x = 0;
cin >> x; // 假设输入 30
// 用 std::find 查找元素
it = find(lt.begin(), lt.end(), x);
// 找到就删除
if (it != lt.end())
{
lt.erase(it); // erase 会删除 it 指向的节点
}
// 打印删除后的结果
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
// 输入30,输出:1 2 3 4 5 6
}
核心知识点:
-
list 迭代器只能 ++ / --,不能 +n; list 是双向迭代器,不是随机迭代器; 不支持:it + 3、it - 2、it[n]; 要往后走,只能循环 ++it
-
insert 是在迭代器前面插入 lt.insert(it, 30);
在 it 指向的节点之前插入; insert 后,原有迭代器不会失效
-
list 可以用 std::find,但效率是 O(N); std::find 适用于所有迭代器类型; 但 list 不支持随机访问,查找只能从头遍历
-
erase(it) 的特点: 删除 it 指向的节点; 只有被删的迭代器失效,其他迭代器没事
正确遍历删除写法:it = lt.erase(it);
总结: list 插入删除效率高 O(1),但查找慢 O(N); insert 在迭代器前插入,erase 删除指定位置
迭代器只支持 ++/--,不支持随机访问
代码示例4:sort、merge
cpp
// list 的排序 sort、反转 reverse、合并 merge
void test_list4()
{
// 1. 创建 list 并插入数据
list<int> lt;
lt.push_back(1);
lt.push_back(20);
lt.push_back(3);
lt.push_back(5);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
// 打印原序列
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
// 输出:1 20 3 5 4 5 6
// 2. list 排序 ------ 必须用成员函数 sort()
// 不能用全局的 sort(lt.begin(), lt.end())
// 因为 list 迭代器不支持随机访问
// 默认:升序
// lt.sort();
// 降序:传 greater<int>() 仿函数
lt.sort(greater<int>());
// 打印排序后(降序)
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
// 输出:20 6 5 5 4 3 1
// 3. list 反转
// 成员函数 reverse():O(N),直接反转链表
// lt.reverse();
// 错误:全局算法 reverse 不能用在 list
// 因为需要随机访问迭代器
// reverse(lt.begin(), lt.end());
// 4. list 合并 merge
// 作用:将两个 **已经有序** 的 list 合并成一个有序 list
std::list<double> first, second;
first.push_back(3.1);
first.push_back(2.2);
first.push_back(2.9);
second.push_back(3.7);
second.push_back(7.1);
second.push_back(1.4);
// merge 前提:两个链表必须 **先排序**
first.sort();
second.sort();
// 合并:把 second 合并到 first 中
// 合并后 second 变空,first 依然有序
first.merge(second);
}
输出结果:
cpp
1 20 3 5 4 5 6
20 6 5 5 4 3 1
核心知识点:
- list 为什么必须用成员函数 sort?
• 全局 std::sort 要求随机访问迭代器(it+1、it+n); list 是双向迭代器,只支持 ++ / --; 所以必须用 list 自带的 sort 成员函数 ;底层是归并排序,时间复杂度 O(N log N)
-
sort 排序规则: lt.sort() → 默认 升序(less<int>); lt.sort(greater<int>()) → 降序
-
reverse 反转: 用 成员函数 lt.reverse(); 不能用全局 reverse()
-
merge 合并(非常重要): 功能:合并两个有序链表; 前提:两个链表必须先排好序
效果: first.merge(second); 合并后 second 变空; first 变成新的有序链表
总结:list 排序必须用成员函数 sort; list 反转必须用成员函数 reverse; merge 必须合并两个有序链表,合并后原链表为空; list 所有操作都不支持随机访问
代码示例5:unique
cpp
// list 的去重:unique
void test_list5()
{
list<int> lt;
lt.push_back(1);
lt.push_back(20);
lt.push_back(3);
lt.push_back(5);
lt.push_back(5);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
// 1. 排序:必须先排序,unique 才能正确去重
lt.sort();
// 排序后:1 3 4 5 5 5 6 20
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
// 2. 去重:删除连续重复的元素
lt.unique();
// 去重后:1 3 4 5 6 20
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
输出结果:
cpp
1 3 4 5 5 5 6 20
1 3 4 5 6 20
核心知识点
-
lt.unique() 作用: 删除连续重复的元素,只保留一个。时间复杂度 O(N)。
-
非常重要:必须先 sort,再 unique: unique 只能去掉连续重复的值。 如果不排序,重复值不连续,去重失败。
-
list 去重标准流程:lt.sort(); // 1. 排序 lt.unique(); // 2. 去重
-
与 vector 对比
• vector 也可以用:
cpp
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
• list 更简单:
cpp
lt.sort();
lt.unique();
总结:unique 只能去连续重复; 正确用法:先 sort,再 unique; list 去重比 vector 更简洁。
代码示例6:splice
cpp
// list 核心王牌接口:splice
// 功能:链表节点的**转移、剪切、拼接**
// 特点:O(1) 操作,只改指针,不拷贝、不删除元素
void test_list6()
{
// 一、把一个链表的节点,全部转移到另一个链表
std::list<int> mylist1, mylist2;
std::list<int>::iterator it;
// mylist1: 1 2 3 4
for (int i = 1; i <= 4; ++i)
mylist1.push_back(i);
// mylist2: 10 20 30
for (int i = 1; i <= 3; ++i)
mylist2.push_back(i * 10);
it = mylist1.begin();
++it; // it 指向 2
// 将 mylist2 所有节点 转移到 mylist1 的 it 位置之前
mylist1.splice(it, mylist2);
// 结果:
// mylist1: 1 10 20 30 2 3 4
// mylist2: 空
// it 依然指向 2(有效)
// -------------------------------------------------------
// 二、同一个链表内部:移动一段节点(调整顺序)
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
// 打印:1 2 3 4 5 6
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
// 输入一个值,比如输入 3
int x = 0;
cin >> x;
it = find(lt.begin(), lt.end(), x);
if (it != lt.end())
{
// 把 [it, lt.end()) 这一段节点
// 剪切移动到 lt.begin() 位置前面(也就是最前面)
lt.splice(lt.begin(), lt, it, lt.end());
}
// 假设输入 3:
// 原链表:1 2 3 4 5 6
// 剪切 [3,4,5,6] 移到开头
// 结果:3 4 5 6 1 2
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
核心知识点
-
splice 是什么? splice = 剪切 + 拼接; 把一个 list 的若干节点,转移到另一个 list(或同 list 的另一位置); 真正 O(1) 操作: 不拷贝元素; 不析构、不构造; 只修改节点指针
-
常用三种用法
1) 转移整个链表:lt1.splice(pos, lt2);
2) 转移单个节点:lt1.splice(pos, lt2, it);
3) 转移一段区间:lt1.splice(pos, lt2, first, last);
- 同链表内部移动(非常实用):lt.splice(lt.begin(), lt, it, lt.end());
在同一个链表里移动一段节点; 依然是 O(1); 常用于:把某一段提前、挪后、翻转、调整顺序
- 重要特性: 转移后,原链表中的节点被移走,不是拷贝; 迭代器依然有效,只是指向的链表变了; 是 list 独有的高效接口,vector 没有
总结:splice 是 list 最强大的接口:节点转移,纯改指针,O(1);可以跨链表转移,也可以在同链表内部移动;不拷贝、不构造、不析构,效率极高
1.2.6 list 的迭代器失效
• 原理:迭代器失效即迭代器所指向的节点无效,该节点被删除了。
• 特点:在 list 中进行插入时不会导致 list 的迭代器失效,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
错误示例:
cpp
void TestListIterator1()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array+sizeof(array)/sizeof(array[0]));
auto it = l.begin();
while (it != l.end())
{
// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
l.erase(it);
++it;
}
}
正确示例:
cpp
void TestListIterator()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array+sizeof(array)/sizeof(array[0]));
auto it = l.begin();
while (it != l.end())
{
// 利用 erase 的返回值更新迭代器
it = l.erase(it);
// 或者使用后置++:l.erase(it++);
}
}
2. list 的模拟实现
2.1 模拟实现 list
要模拟实现 list,必须要熟悉 list 的底层结构以及其接口的含义。
cpp
#include <iostream>
#include <assert.h>
using namespace std;
namespace gxy
{
// List的节点类
template <class T>
struct ListNode
{
ListNode(const T &val = T())
: _pPre(nullptr), _pNext(nullptr), _val(val)
{
}
ListNode<T> *_pPre;
ListNode<T> *_pNext;
T _val;
};
// List的迭代器类
template <class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> *PNode;
typedef ListIterator<T, Ref, Ptr> Self;
ListIterator(PNode pNode = nullptr)
{
_pNode = pNode;
}
ListIterator(const Self &l)
{
_pNode = l._pNode;
}
T &operator*()
{
return _pNode->_val;
}
T *operator->()
{
return &(_pNode->_val);
}
Self &operator++()
{
_pNode = _pNode->_pNext;
return *this;
}
Self operator++(int)
{
Self temp(*this);
_pNode = _pNode->_pNext;
return temp;
}
Self &operator--()
{
_pNode = _pNode->_pPre;
return *this;
}
Self operator--(int)
{
Self temp(*this);
_pNode = _pNode->_pPre;
return temp;
}
bool operator!=(const Self &l) const
{
return _pNode != l._pNode;
}
bool operator==(const Self &l) const
{
return _pNode == l._pNode;
}
PNode _pNode;
};
// list类
template <class T>
class list
{
typedef ListNode<T> Node;
typedef Node *PNode;
public:
typedef ListIterator<T, T &, T *> iterator;
typedef ListIterator<T, const T &, const T *> const_iterator;
public:
///////////////////////////////////////////////////////////////
// List的构造
list()
{
CreateHead();
}
list(int n, const T &value = T())
{
CreateHead();
for (int i = 0; i < n; ++i)
{
push_back(value);
}
_size = n;
}
template <class Iterator>
list(Iterator first, Iterator last)
{
CreateHead();
while (first != last)
{
push_back(*first);
++first;
_size++;
}
}
list(const list<T> &l)
{
CreateHead();
for (auto e : l)
{
push_back(e);
}
_size = l._size;
}
list<T> &operator=(const list<T> l)
{
swap(l);
return *this;
}
~list()
{
clear();
delete _pHead;
_pHead = nullptr;
_size = 0;
}
///////////////////////////////////////////////////////////////
// List Iterator
iterator begin()
{
return iterator(_pHead->_pNext);
}
iterator end()
{
return iterator(_pHead);
}
const_iterator begin() const
{
return const_iterator(_pHead->_pNext);
}
const_iterator end() const
{
return const_iterator(_pHead);
}
///////////////////////////////////////////////////////////////
// List Capacity
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
////////////////////////////////////////////////////////////
// List Access
T &front()
{
return _pHead->_pNext->_val;
}
const T &front() const
{
return _pHead->_pNext->_val;
}
T &back()
{
return _pHead->_pPre->_val;
}
const T &back() const
{
return _pHead->_pPre->_val;
}
////////////////////////////////////////////////////////////
// List Modify
void push_back(const T &val) { insert(end(), val); }
void pop_back() { erase(--end()); }
void push_front(const T &val) { insert(begin(), val); }
void pop_front() { erase(begin()); }
// 在pos位置前插入值为val的节点
iterator insert(iterator pos, const T &val)
{
Node *cur = pos._pNode;
Node *prev = cur->_pPre;
Node *newNode = new Node(val);
prev->_pNext = newNode;
newNode->_pPre = prev;
newNode->_pNext = cur;
cur->_pPre = newNode;
_size++;
return newNode; // 用 newNode 构造 iterator → 返回迭代器 return iterator(newNode);
}
// 删除pos位置的节点,返回该节点的下一个位置
iterator erase(iterator pos)
{
assert(pos != end());
Node *prev = pos._pNode->_pPre;
Node *next = pos._pNode->_pNext;
prev->_pNext = next;
next->_pPre = prev;
delete pos._pNode;
_size--;
return iterator(next);
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
void swap(list<T> &l)
{
std::swap(_pHead, l._pHead);
std::swap(_size, l._size);
}
private:
void CreateHead()
{
_pHead = new Node();
_pHead->_pPre = _pHead;
_pHead->_pNext = _pHead;
_size = 0;
}
PNode _pHead;
size_t _size;
};
};
int main()
{
gxy::list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
for (auto x : lt)
{
cout << x << " ";
}
cout << endl;
lt.pop_back();
for (auto it = lt.begin(); it != lt.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
return 0;
}
2.2 list 的反向迭代器
• 原理:反向迭代器的 ++ 就是正向迭代器的 --,反向迭代器的 -- 就是正向迭代器的 ++。
• 实现方式:反向迭代器的实现可以借助正向迭代器,即:反向迭代器内部可以包含一个正向迭代器,对正向迭代器的接口进行包装即可。
cpp
template<class Iterator>
class ReverseListIterator
{
// 注意:此处typename的作用是明确告诉编译器,Ref是Iterator类中的类型,而不是静态成员变量
// 否则编译器编译时就不知道Ref是Iterator中的类型还是静态成员变量
// 因为静态成员变量也是按照 类名::静态成员变量名 的方式访问的
public:
typedef typename Iterator::Ref Ref;
typedef typename Iterator::Ptr Ptr;
typedef ReverseListIterator<Iterator> Self;
public:
// 构造
ReverseListIterator(Iterator it) : _it(it) {}
// 具有指针类似行为
Ref operator*() {
Iterator temp(_it);
--temp;
return *temp;
}
Ptr operator->() {
return &(operator*());
}
// 迭代器支持移动
Self& operator++() {
--_it;
return *this;
}
Self operator++(int) {
Self temp(*this);
--_it;
return temp;
}
Self& operator--() {
++_it;
return *this;
}
Self operator--(int) {
Self temp(*this);
++_it;
return temp;
}
// 迭代器支持比较
bool operator!=(const Self& l) const {
return _it != l._it;
}
bool operator==(const Self& l) const {
return _it == l._it;
}
Iterator _it;
};
3. list 与 vector 的对比
代码1:vector sort vs list sort 效率对比
cpp
// 测试 vector 和 list 的排序效率
void test_op1()
{
// 设置随机数种子
srand(time(0));
// 测试数据规模:100万 个整数
const int N = 1000000;
list<int> lt1;
vector<int> v;
// 同时向 list 和 vector 插入 100w 个随机数
for (int i = 0; i < N; ++i)
{
auto e = rand() + i;
lt1.push_back(e);
v.push_back(e);
}
// 1. 测试 vector 的全局 sort
int begin1 = clock();
// vector 支持随机访问迭代器 → 可以用 std::sort(快速排序)
sort(v.begin(), v.end());
int end1 = clock();
// 2. 测试 list 成员函数 sort
int begin2 = clock();
// list 不支持随机访问 → 只能用自己的 sort(归并排序)
lt1.sort();
int end2 = clock();
// 打印耗时(单位:ms)
printf("vector sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
}
运行结果:vector sort: 35~60 (取决于电脑);list sort: 60~100
结论:vector 的 sort 通常比 list 快。
核心知识点
- 为什么 vector sort 更快? vector 底层是连续数组; 空间局部性好,CPU 缓存命中率高
std::sort 底层是 快速排序 + 插入排序 混合,极快
- 为什么 list sort 慢一点? list 是链表,节点不连续; 无法利用 CPU 缓存
list::sort 底层是归并排序,无法像快排那样极致优化
- 关键区别: vector:用 全局算法 std::sort ; list:用 成员函数 lt.sort()
原因:list 迭代器不支持随机访问
- 总结:连续内存 > 链表内存:vector 排序更快; vector:std::sort → 快排,速度快
list:list::sort → 归并,速度稍慢 ;数据越大,连续内存优势越明显
代码2:list 直接排序 vs 转 vector 再排序
cpp
// 对比两种排序方式:
// 1. list 直接用成员函数 sort
// 2. list -> vector -> sort -> 拷贝回 list
// 看哪种更快
void test_op2()
{
srand(time(0));
const int N = 1000000;
list<int> lt1;
list<int> lt2;
// 两个链表插入一模一样的 100w 个随机数
for (int i = 0; i < N; ++i)
{
auto e = rand()+i;
lt1.push_back(e);
lt2.push_back(e);
}
// ==========================
// 方式1:list → vector → 排序 → 拷回 list
// ==========================
int begin1 = clock();
// 1. 把 list 拷贝到 vector(连续内存)
vector<int> v(lt2.begin(), lt2.end());
// 2. 对 vector 使用全局快排 std::sort(极快)
sort(v.begin(), v.end());
// 3. 把排好的 vector 再赋值回 list
lt2.assign(v.begin(), v.end());
int end1 = clock();
// ==========================
// 方式2:list 直接调用成员函数 sort
// ==========================
int begin2 = clock();
lt1.sort();
int end2 = clock();
// 打印耗时
printf("list copy vector sort copy list sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
}
运行结果(典型):
cpp
list copy vector sort copy list sort: 40~70
list sort: 70~110
结论:即使多了两次拷贝(list→vector→list),整体依然可能比 list 自带 sort 更快!
核心知识点
- 为什么「拷贝+排序+拷贝」反而可能更快?
vector 内存连续,CPU 缓存命中率极高; std::sort 是快速排序+插入排序的高度优化混合算法
连续内存的排序速度 ≫ 链表排序速度; 两次拷贝的开销,抵不上缓存带来的巨大优势
-
list::sort 慢在哪里? 链表节点不连续,无法有效利用 CPU 缓存;只能用归并排序,无法像快排那样高效; 随机跳转内存,速度天然吃亏
-
这道题想告诉你什么?数据结构的底层物理结构,极大影响效率。
链表优势:任意位置插入、删除、splice 转移 O(1); 链表劣势:遍历、查找、排序 都慢
- 工程经验,真实项目中,如果追求极致排序速度,且数据量大:list → vector → sort → 拷回 list 往往比直接 list.sort() 更快。
总结:vector 连续内存 + std::sort 快排 → 极快; list 链表 + 归并排序 → 较慢
哪怕多两次拷贝,vector 排序依旧可能更快; 链表优势不在排序,而在插入、删除、节点转移
|-------|-----------------------------------------------------------------------|-------------------------------------------|
| 对比项 | vector | list |
| 底层结构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
| 随机访问 | 支持随机访问,访问某个元素效率 O(1) | 不支持随机访问,访问某个元素效率 O(N) |
| 插入和删除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为 O(N);插入时有可能需要增容,增容会开辟新空间、拷贝元素、释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为 O(1) |
| 空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
| 迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
| 迭代器失效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效;删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
| 使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |
通过对 list 容器的全面剖析,我们不仅掌握了它的接口使用,更深入理解了其底层双向循环链表的设计思想。它在插入、删除操作上的 O(1) 效率,是以牺牲随机访问能力为代价的,这也正是算法设计中"没有银弹"的生动体现。
在实际项目中,选择 vector 还是 list,本质上是在连续内存的高效访问与链表结构的灵活操作之间做出权衡。理解每种容器的 trade-off,才能写出更高效、更优雅的 C++ 代码。