目录
[一、 节点结构定义](#一、 节点结构定义)
[二、 list 类的核心成员与接口实现](#二、 list 类的核心成员与接口实现)
[2.1 list 类的成员变量与默认构造](#2.1 list 类的成员变量与默认构造)
[三、 list 反向迭代器实现](#三、 list 反向迭代器实现)
[四、list 与 vector 深度对比:选择合适的容器](#四、list 与 vector 深度对比:选择合适的容器)
[4.1 核心特性对比](#4.1 核心特性对比)
[4.2 适用场景对比](#4.2 适用场景对比)
[vector 适用场景:](#vector 适用场景:)
[list 适用场景:](#list 适用场景:)
[4.3 场景选择对比](#4.3 场景选择对比)
[场景 1:频繁随机访问 ------ 选择 vector](#场景 1:频繁随机访问 —— 选择 vector)
[场景 2:频繁中间插入 ------ 选择 list](#场景 2:频繁中间插入 —— 选择 list)
前言
在上一篇博客中,我为大家介绍了list类的核心原理和接口的使用,本期我们将继续学期list类的模拟实现。话不多说,让我们开始吧!
理解 list
的底层实现,不仅能帮助我们更灵活地使用 list
,还能掌握双向循环链表的设计思想。模拟实现 list
需完成三个核心部分:节点结构定义 、list 类的核心成员与接口实现 、反向迭代器实现 。
一、 节点结构定义
list
的每个节点包含数据、前驱指针、后继指针,因此我们首先定义一个节点模板结构体 ListNode
:
cpp
template <class T>
struct ListNode {
T _data; // 存储数据
ListNode<T>* _prev; // 指向前驱节点
ListNode<T>* _next; // 指向后继节点
// 节点构造函数:初始化数据,前驱和后继指针默认指向 nullptr
ListNode(const T& data = T())
: _data(data)
, _prev(nullptr)
, _next(nullptr)
{}
};
二、 list 类的核心成员与接口实现
list
类的核心成员是头结点指针 (_head
),所有操作(插入、删除、遍历)均围绕头结点展开。以下是 list
类的模板定义及关键接口实现:
2.1 list 类的成员变量与默认构造
cpp
template <class T>
class list {
// 定义节点类型别名,简化代码
typedef ListNode<T> Node;
public:
// -------------------------- 正向迭代器定义 --------------------------
// 迭代器本质是对节点指针的封装,需支持 *、->、++、--、!=、== 等操作
class iterator {
public:
typedef ListNode<T> Node;
typedef iterator self;
// 迭代器构造函数:用节点指针初始化
iterator(Node* node)
: _node(node)
{}
// 解引用:返回节点数据的引用
T& operator*() {
return _node->_data;
}
// 箭头运算符:返回节点数据的指针(用于自定义类型成员访问)
T* 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;
}
// 节点指针(供反向迭代器访问)
Node* _node;
};
// -------------------------- list 核心接口 --------------------------
// 默认构造:创建头结点,形成闭环
list() {
// 初始化头结点,前驱和后继都指向自身
_head = new Node();
_head->_prev = _head;
_head->_next = _head;
}
// 析构函数:释放所有节点(包括头结点)
~list() {
clear(); // 清空有效节点
delete _head; // 释放头结点
_head = nullptr; // 避免野指针
}
// 拷贝构造:深拷贝(用其他 list 初始化当前 list)
list(const list<T>& l) {
// 1. 初始化当前 list 的头结点
_head = new Node();
_head->_prev = _head;
_head->_next = _head;
// 2. 遍历 l 的有效节点,逐个插入到当前 list 尾部
auto it = l.begin();
while (it != l.end()) {
push_back(*it);
++it;
}
}
// 赋值运算符重载:深拷贝(现代写法,利用拷贝构造和 swap)
list<T>& operator=(list<T> l) {
swap(_head, l._head);
return *this;
}
// -------------------------- 迭代器接口 --------------------------
iterator begin() {
// 第一个有效节点是头结点的 next
return iterator(_head->_next);
}
iterator end() {
// 尾迭代器是头结点
return iterator(_head);
}
// -------------------------- 容量接口 --------------------------
bool empty() const {
// 头结点的 next 指向自身,说明无有效节点
return _head->_next == _head;
}
size_t size() const {
size_t count = 0;
auto it = begin();
while (it != end()) {
++count;
++it;
}
return count;
}
// -------------------------- 元素访问接口 --------------------------
T& front() {
// 第一个有效节点的数据
return *begin();
}
const T& front() const {
return *begin();
}
T& back() {
// 最后一个有效节点是头结点的 prev
return *(--end());
}
const T& back() const {
return *(--end());
}
// -------------------------- 元素修改接口 --------------------------
// 头部插入:在头结点和第一个有效节点之间插入
void push_front(const T& val) {
Node* newNode = new Node(val);
Node* first = _head->_next; // 原第一个有效节点
// 调整指针:头结点 <-> newNode <-> first
_head->_next = newNode;
newNode->_prev = _head;
newNode->_next = first;
first->_prev = newNode;
}
// 头部删除:删除第一个有效节点
void pop_front() {
if (empty()) {
return; // 空链表,无需删除
}
Node* first = _head->_next; // 要删除的节点
Node* second = first->_next; // 原第二个有效节点
// 调整指针:头结点 <-> second
_head->_next = second;
second->_prev = _head;
delete first; // 释放删除的节点
}
// 尾部插入:在头结点和最后一个有效节点之间插入
void push_back(const T& val) {
Node* newNode = new Node(val);
Node* last = _head->_prev; // 原最后一个有效节点
// 调整指针:last <-> newNode <-> 头结点
last->_next = newNode;
newNode->_prev = last;
newNode->_next = _head;
_head->_prev = newNode;
}
// 尾部删除:删除最后一个有效节点
void pop_back() {
if (empty()) {
return; // 空链表,无需删除
}
Node* last = _head->_prev; // 要删除的节点
Node* prevLast = last->_prev;// 原倒数第二个有效节点
// 调整指针:prevLast <-> 头结点
prevLast->_next = _head;
_head->_prev = prevLast;
delete last; // 释放删除的节点
}
// 任意位置插入:在 pos 指向的节点之前插入
iterator insert(iterator pos, const T& val) {
Node* newNode = new Node(val);
Node* cur = pos._node; // pos 指向的节点
Node* prev = cur->_prev; // pos 节点的前驱
// 调整指针:prev <-> newNode <-> cur
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = cur;
cur->_prev = newNode;
// 返回指向新插入节点的迭代器
return iterator(newNode);
}
// 任意位置删除:删除 pos 指向的节点,返回下一个节点的迭代器
iterator erase(iterator pos) {
if (pos == end()) {
return end(); // 不能删除尾迭代器(头结点)
}
Node* cur = pos._node; // 要删除的节点
Node* prev = cur->_prev; // 前驱节点
Node* next = cur->_next; // 后继节点
// 调整指针:prev <-> next
prev->_next = next;
next->_prev = prev;
delete cur; // 释放删除的节点
// 返回指向 next 节点的迭代器
return iterator(next);
}
// 清空有效节点(头结点保留)
void clear() {
auto it = begin();
while (it != end()) {
it = erase(it); // 用 erase 的返回值重置迭代器
}
}
// 交换两个 list 的头结点(实现 O(1) 交换)
void swap(list<T>& l) {
std::swap(_head, l._head);
}
private:
Node* _head; // 头结点指针(哨兵节点)
};
三、 list 反向迭代器实现
反向迭代器的核心逻辑是 "复用正向迭代器" ------ 反向迭代器的 ++
对应正向迭代器的 --
,反向迭代器的 --
对应正向迭代器的 ++
。因此,我们可以设计一个模板类ReverseIterator,内部包含一个正向迭代器,通过包装正向迭代器的接口实现反向迭代功能。
cpp
// 反向迭代器模板类:模板参数为正向迭代器类型
template <class Iterator>
class ReverseIterator {
public:
typedef typename Iterator::Ref Ref; // 迭代器指向数据的引用类型(需用 typename 声明是类型)
typedef typename Iterator::Ptr Ptr; // 迭代器指向数据的指针类型
typedef ReverseIterator<Iterator> Self;
// 构造函数:用正向迭代器初始化
ReverseIterator(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& it) const {
return _it == it._it;
}
// 不等比较:正向迭代器是否不同
bool operator!=(const Self& it) const {
return _it != it._it;
}
private:
Iterator _it; // 内部存储的正向迭代器
};
我们还可以在 list
类的public 区域添加反向迭代器的类型定义和接口:
cpp
template <class T>
class list {
// ... 其他成员(节点定义、正向迭代器、核心接口等)...
public:
// 正向迭代器的 Ref 和 Ptr 定义(供反向迭代器使用)
typedef T& Ref;
typedef T* Ptr;
// 反向迭代器类型定义
typedef ReverseIterator<iterator> reverse_iterator;
// 反向迭代器接口
reverse_iterator rbegin() {
// rbegin() 对应正向迭代器的 end()
return reverse_iterator(end());
}
reverse_iterator rend() {
// rend() 对应正向迭代器的 begin()
return reverse_iterator(begin());
}
// ... 其他成员 ...
};
反向迭代器的使用示例如下:
cpp
#include <iostream>
#include "MyList.h" // 包含自定义的 list 实现
using namespace std;
int main() {
MyList::list<int> l = {1, 2, 3, 4, 5}; // 假设自定义 list 命名空间为 MyList
// 反向遍历
cout << "反向遍历 l: ";
auto rit = l.rbegin();
while (rit != l.rend()) {
cout << *rit << " "; // 输出:5 4 3 2 1
++rit;
}
cout << endl;
return 0;
}
四、list 与 vector 深度对比:选择合适的容器
list
和 vector
是 STL 中最常用的两个序列式容器,但由于底层结构不同,它们的特性、效率和适用场景差异极大。掌握两者的对比,是在实际开发中选择正确容器的关键。
4.1 核心特性对比
下表从底层结构、访问效率、插入删除效率等 7 个核心维度对比 list
和 vector
:
对比维度 | vector | list |
---|---|---|
底层结构 | 动态顺序表(一段连续的内存空间) | 带头结点的双向循环链表(非连续内存,节点动态开辟) |
随机访问支持 | 支持(通过下标 [] 或 at() 访问,时间复杂度 O (1)) |
不支持(需通过迭代器遍历,时间复杂度 O (N)) |
插入 / 删除效率 | **1.**头部 / 中间插入 / 删除:需搬移后续元素,时间复杂度 O (N); 2. 尾部插入 / 删除(无扩容):时间复杂度 O (1); 3. 尾部插入(需扩容):需开辟新空间、拷贝元素、释放旧空间,效率低 | **1.**任意位置插入 / 删除(找到位置后):仅修改指针,时间复杂度 O (1); **2.**查找位置需遍历,时间复杂度 O (N)(但插入 / 删除本身效率极高) |
空间利用率 | 1. 连续内存,无节点开销,空间利用率高; **2.**扩容会预留额外空间(如 1.5 倍或 2 倍),可能造成内存浪费 | 1. 每个节点包含数据和两个指针,存在节点开销(小数据类型时开销占比高); **2.**节点动态开辟,易产生内存碎片,空间利用率低 |
缓存利用率 | 高。CPU 缓存基于 "局部性原理",连续内存中的元素会被批量加载到缓存,访问相邻元素时无需重新加载 | 低。节点内存不连续,访问下一个节点时大概率未被加载到缓存,需频繁从内存读取,效率低 |
迭代器类型 | 原生态指针(指向连续内存中的元素) | 对节点指针的封装(需支持 ++ 、-- 操作,指向相邻节点) |
迭代器失效 | **1.**插入元素(尾部插入且无扩容除外):所有迭代器失效(内存重新分配,原地址无效); 2. 删除元素:当前迭代器及后续迭代器失效(元素前移,原地址指向的元素改变) | **1.**插入元素:所有迭代器均有效(仅修改指针,节点地址不变); 2. 删除元素:仅指向被删除节点的迭代器失效,其他迭代器有效 |
4.2 适用场景对比
根据上述特性,list
和 vector
的适用场景有明确区分:
vector 适用场景:
- 需要频繁随机访问元素的场景:如数组排序、二分查找(需通过下标快速定位元素)。
- 元素插入 / 删除主要在尾部的场景:如日志记录(仅需在尾部追加日志)、栈(后进先出,仅操作尾部)。
- 对空间利用率和缓存效率要求高 的场景:如存储大量小数据类型(如
int
、float
),连续内存可减少开销。
list 适用场景:
- 需要频繁在头部或中间插入 / 删除元素的场景:如链表式队列(头部删除、尾部插入)、双向队列(头尾均需操作)、频繁修改的列表(如购物车添加 / 删除商品)。
- 无需随机访问元素的场景:仅需遍历元素,或通过迭代器定位到特定位置后进行修改。
- 元素数量不确定,且需频繁动态调整 的场景:
list
无需扩容,插入 / 删除时仅需分配 / 释放单个节点,避免vector
扩容带来的性能开销。
4.3 场景选择对比
场景 1:频繁随机访问 ------ 选择 vector
cpp
#include <vector>
#include <list>
#include <iostream>
#include <ctime>
using namespace std;
// 测试随机访问效率:通过下标访问第 10000 个元素
void TestRandomAccess() {
const int N = 100000;
vector<int> v(N, 0);
list<int> l(N, 0);
// 测试 vector 随机访问
clock_t start = clock();
for (int i = 0; i < 10000; ++i) {
v[99999] = i; // 直接下标访问,O(1)
}
clock_t end = clock();
cout << "vector 随机访问时间:" << (double)(end - start) / CLOCKS_PER_SEC << "s" << endl;
// 测试 list 随机访问(需遍历,O(N))
start = clock();
for (int i = 0; i < 10000; ++i) {
auto it = l.begin();
advance(it, 99999); // 移动迭代器到第 100000 个元素,需遍历 99999 次
*it = i;
}
end = clock();
cout << "list 随机访问时间:" << (double)(end - start) / CLOCKS_PER_SEC << "s" << endl;
}
int main() {
TestRandomAccess();
// 输出示例:
// vector 随机访问时间:0.0001s
// list 随机访问时间:0.8s(时间差异巨大)
return 0;
}
场景 2:频繁中间插入 ------ 选择 list
cpp
#include <vector>
#include <list>
#include <iostream>
#include <ctime>
using namespace std;
// 测试中间插入效率:在容器中间插入 10000 个元素
void TestInsertInMiddle() {
const int N = 10000;
vector<int> v(1000, 0); // 初始有 1000 个元素
list<int> l(1000, 0);
// 测试 vector 中间插入(需搬移元素,O(N))
clock_t start = clock();
auto vit = v.begin() + 500; // 中间位置
for (int i = 0; i < N; ++i) {
vit = v.insert(vit, i); // 插入后迭代器失效,需重新赋值
}
clock_t end = clock();
cout << "vector 中间插入时间:" << (double)(end - start) / CLOCKS_PER_SEC << "s" << endl;
// 测试 list 中间插入(仅修改指针,O(1))
start = clock();
auto lit = l.begin();
advance(lit, 500); // 定位到中间位置(仅遍历一次)
for (int i = 0; i < N; ++i) {
lit = l.insert(lit, i); // 插入后迭代器有效,仅需重置
}
end = clock();
cout << "list 中间插入时间:" << (double)(end - start) / CLOCKS_PER_SEC << "s" << endl;
}
int main() {
TestInsertInMiddle();
// 输出示例:
// vector 中间插入时间:0.5s
// list 中间插入时间:0.001s(时间差异巨大)
return 0;
}
总结
本文从
list
的基础介绍出发,完整实现了list
的模拟(包括节点结构、正向迭代器、反向迭代器),最后通过与vector
的多维度对比,明确了两者的适用场景。掌握list
的特性与使用技巧,不仅能在合适的场景中提升程序性能,还能加深对链表这种基础数据结构的理解,为后续学习更复杂的容器(如deque
、set等
)打下坚实基础。