C++ STL 详解:list 的介绍使用与模拟实现
文章目录
- [第一部分:list 的介绍与使用](#第一部分:list 的介绍与使用)
- [1. list 的介绍](#1. list 的介绍)
- [2. list 的定义方式](#2. list 的定义方式)
- [3. list 的插入和删除](#3. list 的插入和删除)
- [4. list 的迭代器使用](#4. list 的迭代器使用)
- [5. list 的元素获取](#5. list 的元素获取)
- [6. list 的大小控制](#6. list 的大小控制)
- [7. list 的操作函数](#7. list 的操作函数)
- [8. 第一部分总结](#8. 第一部分总结)
- [第二部分:list 的模拟实现](#第二部分:list 的模拟实现)
- [1. 三个核心类与接口总览](#1. 三个核心类与接口总览)
- [2. 结点类的模拟实现](#2. 结点类的模拟实现)
- [3. 迭代器类的模拟实现](#3. 迭代器类的模拟实现)
- [4. list 的模拟实现](#4. list 的模拟实现)
- [5. 第二部分总结](#5. 第二部分总结)
第一部分:list 的介绍与使用
list 是 STL 中常用的顺序容器,它的底层不是连续数组,而是链表结构
理解 list 时,要把重点放在两个地方:
- list 适合在任意位置做插入和删除
- list 不支持像数组一样通过下标随机访问
1. list 的介绍
list 是一种序列式容器,它的底层一般是双向链表
双向链表中的每个元素都存储在独立结点中,每个结点除了保存数据以外,还会保存两个指针:
- 一个指向前一个结点
- 一个指向后一个结点
所以,list 可以从前往后遍历,也可以从后往前遍历
和 forward_list 相比,list 是双向链表,支持双向迭代;forward_list 是单链表,只能向一个方向迭代
list 的特点大概是:
- 支持在任意位置做插入和删除
- 插入和删除时一般不需要搬移大量元素
- 迭代器支持前后移动
- 不支持下标访问,不能写 lt3
- 查找某个位置时需要从头或从尾遍历
- 每个结点都需要额外指针空间,所以存储小类型数据时空间开销更明显
使用 list 需要包含头文件:
cpp
#include <list>
2. list 的定义方式
list 的定义方式和多数 STL 容器类似
2.1 定义空 list
cpp
list<int> lt1;
表示创建一个存放 int 类型数据的空链表
2.2 定义 n 个 val
cpp
list<int> lt2(10, 2);
表示创建一个 list,其中有 10 个元素,每个元素都是 2
2.3 拷贝构造
cpp
list<int> lt3(lt2);
表示用 lt2 拷贝构造一个新的 list
2.4 使用迭代器区间构造
可以用其他容器或对象的一段迭代器区间来构造 list
cpp
#include <iostream>
#include <list>
#include <string>
using namespace std;
int main()
{
string s("hello world");
// 使用 string 的迭代器区间构造 list<char>
list<char> lt(s.begin(), s.end());
for (auto ch : lt)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
2.5 使用数组区间构造
数组也可以通过首地址和尾后地址构造 list
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int sz = sizeof(arr) / sizeof(arr[0]);
// arr 表示数组首元素地址,arr + sz 表示最后一个元素的后一个位置
list<int> lt(arr, arr + sz);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
return 0;
}
3. list 的插入和删除
list 最擅长的就是插入和删除因为链表结点之间通过指针连接,所以在已知位置插入或删除结点时,不需要搬移大量连续元素
3.1 push_front 和 pop_front
push_front 用来头插数据
pop_front 用来头删数据
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt;
// 每次都插入到链表头部
lt.push_front(0);
lt.push_front(1);
lt.push_front(2);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 2 1 0
// 删除当前第一个元素
lt.pop_front();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 1 0
return 0;
}
头插后,新元素会成为第一个有效元素
头删时,原来的第一个有效元素会被删除
3.2 push_back 和 pop_back
push_back 用来尾插数据
pop_back 用来尾删数据
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt;
// 按顺序尾插
lt.push_back(0);
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 0 1 2 3
// 连续删除两个尾部元素
lt.pop_back();
lt.pop_back();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 0 1
return 0;
}
3.3 insert
list 的 insert 支持多种插入方式
常见写法有三种:
- 在某个迭代器位置前插入一个元素
- 在某个迭代器位置前插入 n 个值为 val 的元素
- 在某个迭代器位置前插入一段迭代器区间 [first, last)
看个例子:
cpp
#include <iostream>
#include <algorithm>
#include <list>
using namespace std;
int main()
{
list<int> lt = {1, 2, 3};
// find 来自 <algorithm>,用于查找值为 2 的位置
auto pos = find(lt.begin(), lt.end(), 2);
// 在 2 的位置前插入 9
lt.insert(pos, 9);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 1 9 2 3
pos = find(lt.begin(), lt.end(), 3);
// 在 3 的位置前插入 2 个 8
lt.insert(pos, 2, 8);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 1 9 2 8 8 3
int extra[] = {7, 7};
pos = find(lt.begin(), lt.end(), 1);
// 将数组区间 [extra, extra + 2) 插入到 1 前面
lt.insert(pos, extra, extra + 2);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 7 7 1 9 2 8 8 3
return 0;
}
注意:find 不是 list 的成员函数,它是 中的通用算法
3.4 erase
list 的 erase 支持两种删除方式:
- 删除某个迭代器位置上的元素
- 删除一段迭代器区间 [first, last) 中的元素
看个例子:
cpp
#include <iostream>
#include <algorithm>
#include <list>
using namespace std;
int main()
{
list<int> lt = {1, 2, 3, 4, 5};
auto pos = find(lt.begin(), lt.end(), 2);
if (pos != lt.end())
{
// 删除值为 2 的元素
lt.erase(pos);
}
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 1 3 4 5
pos = find(lt.begin(), lt.end(), 4);
if (pos != lt.end())
{
// 删除从 4 开始直到末尾的所有元素
lt.erase(pos, lt.end());
}
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 1 3
return 0;
}
因为 list 的结点彼此独立,删除一个结点时一般不会影响其他结点的地址,所以其他未被删除元素的迭代器一般仍然有效
4. list 的迭代器使用
list 支持双向迭代器,所以可以正向遍历,也可以反向遍历
但是 list 不支持随机访问迭代器,所以不能写:
cpp
lt.begin() + 3; // 错误,list 迭代器不支持 + 操作
如果要移动多个位置,需要不断执行 ++it 或使用标准库辅助函数
4.1 begin 和 end
begin() 返回第一个有效元素的迭代器
end() 返回最后一个有效元素后一个位置的迭代器
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt(10, 2);
// 正向迭代器遍历
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
4.2 rbegin 和 rend
rbegin() 返回最后一个有效元素的反向迭代器
rend() 返回第一个有效元素前一个位置的反向迭代器
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt = {1, 2, 3, 4, 5};
// 反向迭代器遍历
list<int>::reverse_iterator rit = lt.rbegin();
while (rit != lt.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl; // 5 4 3 2 1
return 0;
}
5. list 的元素获取
5.1 front 和 back
front() 用来获取第一个元素
back() 用来获取最后一个元素
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt;
lt.push_back(0);
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
cout << lt.front() << endl; // 0
cout << lt.back() << endl; // 4
return 0;
}
如果容器为空,不要直接调用 front() 或 back()
更稳妥的写法是:
cpp
if (!lt.empty())
{
cout << lt.front() << endl;
cout << lt.back() << endl;
}
6. list 的大小控制
6.1 size
size() 用来获取当前容器中的元素个数
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
cout << lt.size() << endl; // 4
return 0;
}
6.2 resize
resize 用来改变容器中的元素个数
规则大概是:
- 如果新大小大于当前 size(),就在末尾补充元素
- 如果传了第二个参数,新增元素使用该值
- 如果没有传第二个参数,新增元素使用默认值
- 如果新大小小于当前 size(),就删除多余元素
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt(5, 3);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 3 3 3 3 3
// 扩大到 7 个元素,新增元素填 6
lt.resize(7, 6);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 3 3 3 3 3 6 6
// 缩小到 2 个元素
lt.resize(2);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 3 3
return 0;
}
6.3 empty
empty() 用来判断容器是否为空
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt;
cout << lt.empty() << endl; // 1,表示为空
lt.push_back(10);
cout << lt.empty() << endl; // 0,表示不为空
return 0;
}
6.4 clear
clear() 用来清空容器
清空后,容器中没有有效元素,size() 变为 0
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt(5, 2);
cout << lt.size() << endl; // 5
// 清空所有有效元素
lt.clear();
cout << lt.size() << endl; // 0
cout << lt.empty() << endl; // 1
return 0;
}
7. list 的操作函数
list 有一些很典型的成员函数,和链表结构关系很密切
7.1 sort
sort() 可以对 list 中的元素做排序,默认按升序排列
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt = {4, 7, 5, 9, 6, 0, 3};
lt.sort(); // 对链表中的元素升序排序
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 0 3 4 5 6 7 9
return 0;
}
注意:list 不能直接使用 std::sort(lt.begin(), lt.end()),因为 std::sort 要求随机访问迭代器,而 list 的迭代器是双向迭代器
所以 list 提供了自己的成员函数 sort()
7.2 splice
splice 用来把一个 list 中的结点转移到另一个 list 的某个位置
常见写法有三种:
- 将整个链表拼接到另一个链表的某个位置
- 将某一个结点拼接到另一个链表的某个位置
- 将某一段迭代器区间拼接到另一个链表的某个位置
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt1(4, 2);
list<int> lt2(4, 6);
// 把 lt2 的全部结点拼接到 lt1 的开头
lt1.splice(lt1.begin(), lt2);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl; // 6 6 6 6 2 2 2 2
list<int> lt3(4, 2);
list<int> lt4(4, 6);
// 把 lt4 的第一个结点转移到 lt3 的开头
lt3.splice(lt3.begin(), lt4, lt4.begin());
for (auto e : lt3)
{
cout << e << " ";
}
cout << endl; // 6 2 2 2 2
list<int> lt5(4, 2);
list<int> lt6(4, 6);
// 把 lt6 的整个区间 [begin, end) 拼接到 lt5 的开头
lt5.splice(lt5.begin(), lt6, lt6.begin(), lt6.end());
for (auto e : lt5)
{
cout << e << " ";
}
cout << endl; // 6 6 6 6 2 2 2 2
return 0;
}
splice 不是复制元素,而是移动链表结点被拼接走的结点会从原容器中消失
7.3 remove
remove(val) 用来删除容器中所有值等于 val 的元素
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt = {1, 4, 3, 3, 2, 2, 3};
// 删除所有值为 3 的结点
lt.remove(3);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 1 4 2 2
return 0;
}
7.4 remove_if
remove_if 用来删除所有满足条件的元素
条件一般由函数、函数对象或 lambda 表达式提供
cpp
#include <iostream>
#include <list>
using namespace std;
bool single_digit(const int& val)
{
return val < 10;
}
int main()
{
list<int> lt = {10, 4, 7, 18, 2, 5, 9};
// 删除所有小于 10 的元素
lt.remove_if(single_digit);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 10 18
return 0;
}
也可以用 lambda 写得更直接:
cpp
lt.remove_if([](int x) {
return x < 10;
});
7.5 unique
unique() 用来删除连续重复元素
注意:它只删除"相邻且相等"的重复元素,不会自动删除分散在各处的相同元素
所以,如果想实现真正意义上的去重,一般先排序,再调用 unique()
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt = {1, 4, 3, 3, 2, 2, 3};
lt.sort(); // 排序后,相同元素会变成连续状态
lt.unique(); // 删除连续重复元素
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 1 2 3 4
return 0;
}
7.6 merge
merge 用来合并两个有序 list,并保持合并后的结果仍然有序
使用 merge 前,两个链表都要已经有序
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt1 = {3, 8, 1};
list<int> lt2 = {6, 2, 9, 5};
lt1.sort(); // 1 3 8
lt2.sort(); // 2 5 6 9
// 将 lt2 合并进 lt1,合并后 lt2 中的结点会被转移走
lt1.merge(lt2);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl; // 1 2 3 5 6 8 9
return 0;
}
merge 和归并排序中的合并过程很像
7.7 reverse
reverse() 用来将链表中的元素顺序逆置
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt = {1, 2, 3, 4, 5};
lt.reverse(); // 逆置链表顺序
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // 5 4 3 2 1
return 0;
}
7.8 assign
assign 用来给容器重新分配内容,会替换掉原来的所有内容
常见写法有两种:
- 分配 n 个值为 val 的元素
- 分配一段迭代器区间 [first, last) 中的内容
cpp
#include <iostream>
#include <string>
#include <list>
using namespace std;
int main()
{
list<char> lt(3, 'a');
// 用 3 个 'b' 替换原内容
lt.assign(3, 'b');
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // b b b
string s("hello world");
// 用 string 的迭代器区间替换 list 的内容
lt.assign(s.begin(), s.end());
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; // h e l l o w o r l d
return 0;
}
7.9 swap
swap 用来交换两个 list 的内容
cpp
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lt1(4, 2);
list<int> lt2(4, 6);
// 交换两个链表的内容
lt1.swap(lt2);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl; // 6 6 6 6
for (auto e : lt2)
{
cout << e << " ";
}
cout << endl; // 2 2 2 2
return 0;
}
8. 第一部分总结
list 的核心特点可以记成:
- 底层是双向链表
- 支持头插、头删、尾插、尾删
- 支持在任意迭代器位置插入和删除
- 支持正向迭代器和反向迭代器
- 支持 front() 和 back() 拿到首尾元素
- 支持 size()、resize()、empty()、clear() 控制大小
- 支持 sort()、splice()、remove()、remove_if()、unique()、merge()、reverse()、assign()、swap() 等链表特色操作
- 不支持下标随机访问
- 适合频繁做任意位置插入和删除
- 查找和定位某个位置一般需要遍历
如果需要频繁在中间插入和删除,list 很合适;如果主要需求是按下标快速访问,则应考虑连续空间容器
第二部分:list 的模拟实现
这部分模拟实现一个简化版 list
标准库中的 list 底层一般是带头双向循环链表模拟实现时,我们也采用这种结构
带头双向循环链表的特点是:
- 有一个不存储有效数据的头结点
- 头结点的 _next 指向第一个有效结点
- 头结点的 _prev 指向最后一个有效结点
- 空链表中,头结点的 _next 和 _prev 都指向自己
- 最后一个有效结点的 _next 指向头结点
- 第一个有效结点的 _prev 指向头结点
这种结构可以统一处理头插、尾插、头删、尾删等情况,避免大量特殊判断
1. 三个核心类与接口总览
模拟实现 list 需要三个核心类:
- 结点类 _list_node
- 迭代器类 _list_iterator
- 容器类 list
先看整体接口:
cpp
#pragma once
#include <cassert>
#include <algorithm>
namespace cl
{
// list 的结点类:保存数据以及前后指针
template<class T>
struct _list_node
{
_list_node(const T& val = T());
T _val; // 数据域
_list_node<T>* _next; // 指向后一个结点
_list_node<T>* _prev; // 指向前一个结点
};
// list 的迭代器类:对结点指针进行封装
template<class T, class Ref, class Ptr>
struct _list_iterator
{
typedef _list_node<T> node;
typedef _list_iterator<T, Ref, Ptr> self;
_list_iterator(node* pnode);
self operator++(); // 前置++
self operator--(); // 前置--
self operator++(int); // 后置++
self operator--(int); // 后置--
bool operator==(const self& s) const;
bool operator!=(const self& s) const;
Ref operator*();
Ptr operator->();
node* _pnode; // 当前迭代器指向的结点
};
template<class T>
class list
{
public:
typedef _list_node<T> node;
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
// 默认成员函数
list();
list(const list<T>& lt);
list<T>& operator=(list<T> lt);
~list();
// 迭代器相关函数
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
// 访问容器相关函数
T& front();
T& back();
const T& front() const;
const T& back() const;
// 插入、删除函数
void insert(iterator pos, const T& x);
iterator erase(iterator pos);
void push_back(const T& x);
void pop_back();
void push_front(const T& x);
void pop_front();
// 其他函数
size_t size() const;
void resize(size_t n, const T& val = T());
void clear();
bool empty() const;
void swap(list<T>& lt);
private:
node* _head; // 指向头结点
};
}
注意:为了避免和标准库中的 std::list 冲突,自定义容器放在 cl 命名空间中
2. 结点类的模拟实现
list 底层每个结点都需要保存三个信息:
- 当前结点的数据
- 指向下一个结点的指针
- 指向上一个结点的指针
所以结点类可以这样设计:
cpp
template<class T>
struct _list_node
{
_list_node(const T& val = T())
: _val(val)
, _next(nullptr)
, _prev(nullptr)
{}
T _val; // 当前结点存储的数据
_list_node<T>* _next; // 后继指针
_list_node<T>* _prev; // 前驱指针
};
构造结点时,如果没有传入数据,就使用 T() 构造默认值
比如:
- int() 是 0
- string() 是空字符串
- 自定义类型会调用自己的默认构造函数
3. 迭代器类的模拟实现
3.1 为什么 list 需要单独实现迭代器类
连续空间容器可以直接用原生指针模拟迭代器,因为指针自增就能移动到下一个元素
但是 list 的结点在内存中不是连续存放的
如果有一个结点指针 node* p,执行:
cpp
++p;
它并不会跳到链表的下一个结点,而是按内存地址向后移动一个 node 大小的位置,这显然不是链表遍历想要的效果
链表中真正的"下一个结点"应该是:
cpp
p = p->_next;
真正的"上一个结点"应该是:
cpp
p = p->_prev;
所以 list 需要把结点指针封装成一个迭代器类,并重载 ++、--、*、-> 等运算符,让它用起来像普通指针一样
3.2 迭代器类模板参数说明
迭代器类设计成三个模板参数:
cpp
template<class T, class Ref, class Ptr>
struct _list_iterator;
这三个参数的含义是:
- T:结点中存储的数据类型
- Ref:解引用后返回的引用类型
- Ptr:箭头运算符返回的指针类型
这样就可以同时支持普通迭代器和 const 迭代器:
cpp
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
普通迭代器解引用后返回 T&,允许修改数据
const 迭代器解引用后返回 const T&,只允许读取数据
3.3 迭代器构造函数
迭代器里面其实就保存着一个结点指针
cpp
template<class T, class Ref, class Ptr>
_list_iterator<T, Ref, Ptr>::_list_iterator(node* pnode)
: _pnode(pnode)
{}
创建迭代器时,只需要传入它要指向的结点地址
3.4 前置 ++ 和后置 ++
前置 ++ 的规则是:先移动,再返回移动后的对象
cpp
template<class T, class Ref, class Ptr>
typename _list_iterator<T, Ref, Ptr>::self
_list_iterator<T, Ref, Ptr>::operator++()
{
_pnode = _pnode->_next; // 移动到后一个结点
return *this; // 返回移动后的迭代器
}
后置 ++ 的规则是:先保存旧值,再移动,最后返回旧值
cpp
template<class T, class Ref, class Ptr>
typename _list_iterator<T, Ref, Ptr>::self
_list_iterator<T, Ref, Ptr>::operator++(int)
{
self tmp(*this); // 保存移动前的位置
_pnode = _pnode->_next; // 移动到后一个结点
return tmp; // 返回移动前的迭代器
}
这里的 self 是当前迭代器类型的别名:
cpp
typedef _list_iterator<T, Ref, Ptr> self;
3.5 前置 -- 和后置 --
前置 -- 的规则是:先移动到前一个结点,再返回当前对象
cpp
template<class T, class Ref, class Ptr>
typename _list_iterator<T, Ref, Ptr>::self
_list_iterator<T, Ref, Ptr>::operator--()
{
_pnode = _pnode->_prev; // 移动到前一个结点
return *this; // 返回移动后的迭代器
}
后置 -- 的规则是:先保存旧位置,再移动到前一个结点,最后返回旧位置
cpp
template<class T, class Ref, class Ptr>
typename _list_iterator<T, Ref, Ptr>::self
_list_iterator<T, Ref, Ptr>::operator--(int)
{
self tmp(*this); // 保存移动前的位置
_pnode = _pnode->_prev; // 移动到前一个结点
return tmp; // 返回移动前的迭代器
}
3.6 == 和 != 运算符重载
判断两个迭代器是否相等,其实就是判断它们是否指向同一个结点
cpp
template<class T, class Ref, class Ptr>
bool _list_iterator<T, Ref, Ptr>::operator==(const self& s) const
{
return _pnode == s._pnode; // 指向同一结点则相等
}
template<class T, class Ref, class Ptr>
bool _list_iterator<T, Ref, Ptr>::operator!=(const self& s) const
{
return _pnode != s._pnode; // 指向不同结点则不相等
}
3.7 * 运算符重载
迭代器解引用时,要得到当前结点中的数据
cpp
template<class T, class Ref, class Ptr>
Ref _list_iterator<T, Ref, Ptr>::operator*()
{
return _pnode->_val; // 返回当前结点的数据
}
返回类型使用 Ref,这样普通迭代器返回 T&,const 迭代器返回 const T&
3.8 -> 运算符重载
如果链表中存储的是自定义类型,经常需要通过迭代器访问对象成员
比如:
cpp
struct Date
{
int _year;
int _month;
int _day;
};
使用时可能希望这样写:
cpp
list<Date> lt;
auto it = lt.begin();
cout << it->_year << endl;
为了支持这种写法,需要重载 operator->
cpp
template<class T, class Ref, class Ptr>
Ptr _list_iterator<T, Ref, Ptr>::operator->()
{
return &_pnode->_val; // 返回当前结点中数据对象的地址
}
从逻辑上说,it->_year 相当于:
cpp
it.operator->()->_year;
看起来要有两个箭头,但编译器为了提高可读性,对 operator-> 做了特殊处理,所以实际写一个 -> 就可以
4. list 的模拟实现
4.1 构造函数
模拟实现的 list 使用带头双向循环链表
构造空链表时,需要创建一个头结点,并让它的前后指针都指向自己
cpp
template<class T>
list<T>::list()
{
_head = new node; // 申请头结点,头结点不存有效数据
_head->_next = _head; // 空链表中,头结点后继指向自己
_head->_prev = _head; // 空链表中,头结点前驱指向自己
}
这样设计后,空链表不是 nullptr,而是一个只有头结点的环
4.2 拷贝构造函数
拷贝构造要创建一个新的链表对象,不能直接共享原链表的结点
思路:
- 先创建自己的头结点
- 初始化为空的循环链表
- 遍历原链表
- 把原链表中的数据一个个尾插到新链表中
cpp
template<class T>
list<T>::list(const list<T>& lt)
{
_head = new node; // 新对象有自己的头结点
_head->_next = _head;
_head->_prev = _head;
for (const auto& e : lt)
{
push_back(e); // 把原链表中的数据逐个尾插进来
}
}
这里同样是深拷贝:新链表中的每个结点都是重新申请的
4.3 赋值运算符重载
赋值运算符可以用传统写法:先清空当前链表,再把右侧链表的数据尾插过来
cpp
template<class T>
list<T>& list<T>::operator=(const list<T>& lt)
{
if (this != <)
{
clear(); // 清理当前链表中的有效结点
for (const auto& e : lt)
{
push_back(e); // 将右侧链表的数据逐个尾插进来
}
}
return *this; // 支持连续赋值
}
也可以使用现代写法:传值 + 交换
cpp
template<class T>
list<T>& list<T>::operator=(list<T> lt)
{
swap(lt); // 当前对象和临时拷贝交换资源
return *this;
}
现代写法的执行过程是:
- 参数 lt 通过传值方式接收,会先调用拷贝构造
- 当前对象和这份临时拷贝交换 _head
- 函数结束后,临时对象析构,释放旧资源
这种写法代码短,而且自然处理深拷贝
4.4 析构函数
析构时要释放所有结点
因为有效结点可以由 clear() 删除,所以析构函数可以复用 clear()
cpp
template<class T>
list<T>::~list()
{
clear(); // 删除所有有效结点,只剩头结点
delete _head; // 释放头结点
_head = nullptr; // 避免野指针
}
4.5 begin 和 end
对于带头双向循环链表:
- begin() 要返回第一个有效结点
- end() 要返回头结点
因为最后一个有效结点的后一个位置就是头结点
cpp
template<class T>
typename list<T>::iterator list<T>::begin()
{
return iterator(_head->_next); // 第一个有效结点
}
template<class T>
typename list<T>::iterator list<T>::end()
{
return iterator(_head); // 头结点作为尾后位置
}
const 版本:
cpp
template<class T>
typename list<T>::const_iterator list<T>::begin() const
{
return const_iterator(_head->_next);
}
template<class T>
typename list<T>::const_iterator list<T>::end() const
{
return const_iterator(_head);
}
空链表中 _head->_next == _head,所以 begin() == end()
4.6 front 和 back
front() 返回第一个有效元素
back() 返回最后一个有效元素
cpp
template<class T>
T& list<T>::front()
{
return *begin(); // begin 指向第一个有效结点
}
template<class T>
T& list<T>::back()
{
return *(--end()); // end 是头结点,--end() 是最后一个有效结点
}
const 版本:
cpp
template<class T>
const T& list<T>::front() const
{
return *begin();
}
template<class T>
const T& list<T>::back() const
{
return *(--end());
}
链表为空时,不要直接调用 front() 或 back()
4.7 insert
insert(pos, x) 表示在 pos 位置之前插入一个新结点
大致步骤:
- 通过迭代器拿到当前位置结点 cur
- 找到 cur 的前一个结点 prev
- 创建新结点 newnode
- 连接 prev 和 newnode
- 连接 newnode 和 cur
cpp
template<class T>
void list<T>::insert(iterator pos, const T& x)
{
assert(pos._pnode); // 确保 pos 内部结点指针有效
node* cur = pos._pnode; // pos 当前指向的结点
node* prev = cur->_prev; // pos 前一个结点
node* newnode = new node(x); // 创建新结点
// prev <-> newnode
prev->_next = newnode;
newnode->_prev = prev;
// newnode <-> cur
newnode->_next = cur;
cur->_prev = newnode;
}
这个函数既能拿来头插,也能拿来尾插
- 头插:insert(begin(), x)
- 尾插:insert(end(), x)
4.8 erase
erase(pos) 删除 pos 位置的结点
大致步骤:
- 检查 pos 合法
- 不能删除 end(),因为 end() 对应的是头结点
- 找到当前结点 cur
- 找到前一个结点 prev 和后一个结点 next
- 释放 cur
- 让 prev 和 next 重新连接
- 返回删除位置的下一个迭代器
cpp
template<class T>
typename list<T>::iterator list<T>::erase(iterator pos)
{
assert(pos._pnode); // pos 必须指向有效结点地址
assert(pos != end()); // 不能删除头结点
node* cur = pos._pnode; // 待删除结点
node* prev = cur->_prev; // 待删除结点的前一个结点
node* next = cur->_next; // 待删除结点的后一个结点
// 先连接前后结点,让链表跳过 cur
prev->_next = next;
next->_prev = prev;
delete cur; // 释放被删除结点
return iterator(next); // 返回删除位置之后的迭代器
}
返回 iterator(next) 很重要边遍历边删除时,可以这样写:
cpp
auto it = lt.begin();
while (it != lt.end())
{
if (*it % 2 == 0)
{
it = lt.erase(it); // 删除后接住下一个位置
}
else
{
++it;
}
}
4.9 push_back 和 pop_back
已经有了 insert 和 erase,尾插尾删可以直接复用
cpp
template<class T>
void list<T>::push_back(const T& x)
{
insert(end(), x); // 在头结点前插入,即尾插
}
template<class T>
void list<T>::pop_back()
{
erase(--end()); // 删除头结点前一个结点,即尾删
}
在带头双向循环链表里,end() 是头结点,--end() 就是最后一个有效结点
4.10 push_front 和 pop_front
头插头删也可以复用 insert 和 erase
cpp
template<class T>
void list<T>::push_front(const T& x)
{
insert(begin(), x); // 在第一个有效结点前插入
}
template<class T>
void list<T>::pop_front()
{
erase(begin()); // 删除第一个有效结点
}
4.11 size
如果 list 中没有额外保存元素个数,那么 size() 只能通过遍历统计
cpp
template<class T>
size_t list<T>::size() const
{
size_t sz = 0;
const_iterator it = begin();
while (it != end())
{
++sz; // 每经过一个有效结点,计数加一
++it;
}
return sz;
}
也可以在 list 中额外增加一个 _size 成员变量,用它实时记录结点个数,这样 size() 就能做到常数时间
不过如果增加 _size,所有插入、删除、清空、交换等函数都要同步维护它
4.12 resize
resize(n, val) 的规则是:
- 如果当前元素个数小于 n,就在尾部插入值为 val 的结点,直到大小为 n
- 如果当前元素个数大于 n,就只保留前 n 个有效数据,删除后面的结点
实现时不建议先调用一次 size() 再处理,因为链表 size() 本身可能需要遍历可以一边遍历一边统计
cpp
template<class T>
void list<T>::resize(size_t n, const T& val)
{
iterator it = begin();
size_t len = 0;
// 尽量向后走 n 个有效结点
while (len < n && it != end())
{
++len;
++it;
}
if (len == n)
{
// 当前有效结点个数大于或等于 n,删除 it 之后的所有结点
while (it != end())
{
it = erase(it);
}
}
else
{
// 当前有效结点个数小于 n,继续尾插 val
while (len < n)
{
push_back(val);
++len;
}
}
}
这里重点看这几个地方:
- 遍历过程中用 len 记录已经走过的有效结点个数
- 如果走够了 n 个结点,后面的都删掉
- 如果链表已经走完但 len < n,说明需要继续尾插
4.13 clear
clear() 用来清空链表中的有效结点,但要保留头结点
cpp
template<class T>
void list<T>::clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it); // erase 返回下一个有效位置
}
}
清空完成后,链表又回到只有头结点的空链表状态
4.14 empty
空链表中只有一个头结点,此时 begin() 和 end() 相等
cpp
template<class T>
bool list<T>::empty() const
{
return begin() == end();
}
也可以理解为:
text
_head->_next == _head
4.15 swap
list 对象中真正维护链表的是 _head 指针
所以交换两个链表,只需要交换它们的头指针
cpp
template<class T>
void list<T>::swap(list<T>& lt)
{
::swap(_head, lt._head); // 调用全局 swap,交换两个头指针
}
这里使用 ::swap,是为了明确调用全局作用域中的 swap,避免和当前成员函数 swap 混淆
5. 第二部分总结
模拟实现 list,重点记这些:
- list 底层可以设计成带头双向循环链表
- 结点类中保存数据、前驱指针、后继指针
- 空链表不是空指针,而是头结点自己指向自己
- list 的结点不连续,不能直接用原生指针当迭代器
- 迭代器类需要封装结点指针,并重载 ++、--、*、->、==、!=
- 普通迭代器和 const 迭代器可以通过 Ref、Ptr 两个模板参数区分
- begin() 返回头结点的后一个结点
- end() 返回头结点
- front() 可以复用 begin()
- back() 可以复用 --end()
- insert() 的其实是新建结点并修改四条指针关系
- erase() 的其实是释放目标结点并重新连接前后结点
- push_back()、pop_back()、push_front()、pop_front() 都可以复用 insert() 和 erase()
- 如果没有 _size 成员,size() 需要遍历统计
- resize() 要根据当前有效结点数量决定删除多余结点还是继续尾插
- clear() 删除所有有效结点,但保留头结点
- empty() 可以通过 begin() == end() 判断
- swap() 只需要交换两个链表的头指针
list 的核心是结点连接和迭代器封装把带头双向循环链表的结构理解清楚后,list 的头插、尾插、删除、遍历、清空和交换等接口都会变得很自然