当前位置: 首页 > news >正文

C++基础:(十三)list类的模拟实现

目录

前言

一、 节点结构定义

二、 list 类的核心成员与接口实现

2.1 list 类的成员变量与默认构造

三、 list 反向迭代器实现

四、list 与 vector 深度对比:选择合适的容器

4.1 核心特性对比

4.2 适用场景对比

vector 适用场景:

list 适用场景:

4.3 场景选择对比

场景 1:频繁随机访问 —— 选择 vector

场景 2:频繁中间插入 —— 选择 list

总结


前言

        在上一篇博客中,我为大家介绍了list类的核心原理和接口的使用,本期我们将继续学期list类的模拟实现。话不多说,让我们开始吧!


        理解 list 的底层实现,不仅能帮助我们更灵活地使用 list,还能掌握双向循环链表的设计思想。模拟实现 list 需完成三个核心部分:节点结构定义list 类的核心成员与接口实现反向迭代器实现

一、 节点结构定义

    list 的每个节点包含数据、前驱指针、后继指针,因此我们首先定义一个节点模板结构体 ListNode

template <class T>
struct ListNode {T _data;               // 存储数据ListNode<T>* _prev;    // 指向前驱节点ListNode<T>* _next;    // 指向后继节点// 节点构造函数:初始化数据,前驱和后继指针默认指向 nullptrListNode(const T& data = T()): _data(data), _prev(nullptr), _next(nullptr){}
};

二、 list 类的核心成员与接口实现

    list 类的核心成员是头结点指针_head),所有操作(插入、删除、遍历)均围绕头结点展开。以下是 list 类的模板定义及关键接口实现:

2.1 list 类的成员变量与默认构造

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() {// 第一个有效节点是头结点的 nextreturn 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() {// 最后一个有效节点是头结点的 prevreturn *(--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 <-> curprev->_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 <-> nextprev->_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,内部包含一个正向迭代器,通过包装正向迭代器的接口实现反向迭代功能。

// 反向迭代器模板类:模板参数为正向迭代器类型
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 区域添加反向迭代器的类型定义和接口:

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());}// ... 其他成员 ...
};

        反向迭代器的使用示例如下:

#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

对比维度vectorlist
底层结构动态顺序表(一段连续的内存空间)带头结点的双向循环链表(非连续内存,节点动态开辟)
随机访问支持支持(通过下标 [] 或 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 适用场景:
  1. 需要频繁随机访问元素的场景:如数组排序、二分查找(需通过下标快速定位元素)。
  2. 元素插入 / 删除主要在尾部的场景:如日志记录(仅需在尾部追加日志)、栈(后进先出,仅操作尾部)。
  3. 对空间利用率和缓存效率要求高的场景:如存储大量小数据类型(如 intfloat),连续内存可减少开销。
list 适用场景:
  1. 需要频繁在头部或中间插入 / 删除元素的场景:如链表式队列(头部删除、尾部插入)、双向队列(头尾均需操作)、频繁修改的列表(如购物车添加 / 删除商品)。
  2. 无需随机访问元素的场景:仅需遍历元素,或通过迭代器定位到特定位置后进行修改。
  3. 元素数量不确定,且需频繁动态调整的场景:list 无需扩容,插入 / 删除时仅需分配 / 释放单个节点,避免 vector 扩容带来的性能开销。

4.3 场景选择对比

场景 1:频繁随机访问 —— 选择 vector
#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
#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 的特性与使用技巧,不仅能在合适的场景中提升程序性能,还能加深对链表这种基础数据结构的理解,为后续学习更复杂的容器(如 dequeset等)打下坚实基础。

http://www.dtcms.com/a/481658.html

相关文章:

  • 【网络编程】从数据链路层帧头到代理服务器:解析路由表、MTU/MSS、ARP、NAT 等网络核心技术
  • 北京网站seowyhseo网站模板但没有后台如何做网站
  • 对接世界职业院校技能大赛标准,唯众打造高质量云计算实训室
  • 利用人工智能、数字孪生、AR/VR 进行军用飞机维护
  • [特殊字符] Maven 编译报错「未与 -source 8 一起设置引导类路径」完美解决方案(以芋道项目为例)
  • 【CV】泊松图像融合
  • 云智融合:人工智能与云计算融合实践指南
  • Maven创建Java项目实战全流程
  • 泉州市住房与城乡建设网站wordpress弹出搜索
  • [创业之路-691]:历史与现实的镜鉴:从三国纷争到华为铁三角的系统性启示
  • 时序数据库选型革命:深入解析Apache IoTDB的架构智慧与实战指南
  • 南通网站制作建设手机网页设计软件下载
  • OpenAI推出即时支付功能,ChatGPT将整合电商能力|技术解析与行业影响
  • 小杰深度学习(seventeen)——视觉-经典神经网络——MObileNetV3
  • 线性代数 | 要义 / 本质 (下篇)
  • C# 预处理指令 (# 指令) 详解
  • 有趣的机器学习-利用神经网络来模拟“古龙”写作风格的输出器
  • AI破解数学界遗忘谜题:GPT-5重新发现尘封二十年的埃尔德什问题解法
  • ui网站推荐如何建网站不花钱
  • Java版自助共享空间系统,打造高效无人值守智慧实体门店
  • 《超越单链表的局限:双链表“哨兵位”设计模式,如何让边界处理代码既优雅又健壮?》
  • HENGSHI SENSE 6.0技术白皮书:基于HQL语义层的Agentic BI动态计算引擎架构解析
  • C#实现MySQL→Clickhouse建表语句转换工具
  • 禁止下载app网站东莞网
  • MySQL数据库精研之旅第十九期:存储过程,数据处理的全能工具箱(二)
  • Ubuntu Linux 服务器快速安装 Docker 指南
  • Linux 信号捕捉与软硬中断
  • Linux NTP配置全攻略:从客户端到服务端
  • 二分查找专题总结:从数组越界到掌握“两段性“
  • aws ec2防ssh爆破, aws服务器加固, 亚马逊服务器ssh安全,防止ip扫描ssh。 aws安装fail2ban, ec2配置fail2ban