探索解析C++ STL中的 list:双向链表的高效实现与迭代器

探索解析C++ STL中的 list:双向链表的高效实现与迭代器

前引:链表作为一种基础数据结构,其非连续内存存储特性使其在频繁的插入和删除操作中具有O(1)的时间复杂度优势,这与数组类型容器形成了鲜明对比。然而,这种优势的背后也伴随着随机访问性能的牺牲和额外的内存开销。本文将从底层实现原理出发,深入探讨`std::list`的内存模型、迭代器特性、以及与其他STL容器的性能对比。我们将通过详细的代码示例和性能分析,帮助读者全面理解何时选择`std::list`,如何高效使用其接口,以及在实际项目中如何权衡其优缺点。无论您是希望深化对STL理解的C++进阶开发者,还是正在学习数据结构与算法的学生,本文都将为您提供系统性的技术洞察和实践指导。让我们一同探索这一经典数据结构在现代C++中的精妙实现

【注意:本文的重点一是知道list的结构,二是迭代器的实现】

目录

List介绍

List实例化

尾插元素

访问元素

指定位置前插入

指定位置删除

排序(升序)

​编辑 排序(降序)

拼接 

去重

反转

注意

模拟实现

list的结构

尾插

尾删

头插

头删

 拷贝构造

赋值运算符重载

迭代器

迭代器结构

begin()、end()

关系比较 !=

运算符重载 *

 运算符重载 ++

无法修改的迭代器

方法(1)

 方法(2)

总结:


List介绍

List也是C++标准模板库(STL)中的一种容器,它的内存存储特点与string、vector不同,它的存储不是连续的,List将元素存储在不连续的内存中,通过指针连接前一个节点和后一个节点。综合来说:就是另一种vector:只是内存存储变成了不连续,下面我来介绍它的各种常用接口!

List实例化

list<类型> 就是它的数据类型,后面跟上变量名即可,例如:

//list实例化 list<int> V;
//list实例化 list<int> V{ 1,2,3,4,5 };

如果想在实例化的时候放上元素,不能和vector、string那样,例如:

尾插元素

使用和之前的两个容器string与vector一样,例如:

//list实例化 list<int> V; //尾插元素 V.push_back(1); V.push_back(2); V.push_back(3);

访问元素

在C++中支持迭代器的容器那就支持auto,而不支持迭代器的容器很少:

stack      queue     priority_queue

所以我们可以通过迭代器去访问元素,例如:

问:为何不是连续的空间,可以使用“++”?

首先“++”是在迭代器里面用了运算符重载(因此只支持在迭代器里面使用),例如:

//迭代器读\写 auto it = V.begin(); while (it != V.end()) { cout << *it << " "; it++; } cout << endl; //auto的底层就是迭代器 for (auto e : V) { cout << e << " "; } cout << endl;

指定位置前插入

这个就是 insert 接口,我们直接看:

//指定位置前插入 V.insert(V.begin(), 5);

注意:list 的内存是不连续的,不能直接使用“+”,例如:

注意:insert 之后对于List的迭代器是不会失效的,例如:

指定位置删除

//指定位置删除 V.erase(V.begin());

注意:使用erase之后,如果不接收返回值,那么迭代器就会失效,例如:

排序(升序)

List的排序属于稳定排序(n logn),例如:

v.sort();

 排序(降序)

V.sort(greater<int>());

拼接 

拼接支持:整体拼接、单个元素拼接、范围拼接,具体的我们实战的时候再去了解

拼接我们在合成多个链表时可以使用,比如:先拼接多个链表再用排序,这样就实现了整体有序

整体拼接: 

//拼接V2在V1的末尾 V1.splice(V1.end(), V2);

 单个元素拼接:

例如:将  V2中 it2  指向的元素拼接到   V1的 it1  前面

去重

去重的前提是元素已经有序

V2.unique();

反转

V2.reverse();

注意:

pos找到的是5的位置,而reverse翻转的是“闭区间,开区间”的元素,也就是0~4

例如:

注意

对于连续存储的 string、Vector我们可以通过 ++ 来移动获得下一个元素\地址

虽然对于 list  属于非连续的存储,我们还是可以使用 ++ ,这是因为支持了运算符重载,++ 的本质是 ->next,比如我们获取 pos 位置的下一个地址应该用 ++ 而不是 +1

模拟实现

list的结构

List属于不连续的存储,通过指针来连接每个空间,这有点像我们的链表,所以我们需要一个节点结构:节点包含 prev 、next、val

//节点结构 template<class T> struct list_node { public: //构造 list_node(const T& date = T()) : next(nullptr) , prev(nullptr) , val(date) { } //节点成员 list_node<T>* next; list_node<T>* prev; T val; };

为何使用struct?因为这是节点,后面有头节点访问这个节点,私有的不能被访问到

除了节点结构外,我们还需要一个指针去指向这个节点,这个节点我们就为头节点:

 //头节点 template<class T> class list { typedef list_node<T> Node; public: list() :Head(nullptr) { //开辟头节点 Head = new Node; Head->next = Head; Head->prev = Head; } private: Node* Head; }; }

节点和头节点的关系梳理:

尾插

双向链表的尾插很简单,头节点的前一个就是尾

//尾插 void push_back(const T& date) { //先开辟空间 Node* tmp = new Node(date); //确认关系 Node* pc = Head->prev; pc->next = tmp; tmp->prev = pc; tmp->next = Head; Head->prev = tmp; }

效果展示:

尾删

尾删注意:如果只有一个头结点的情况下,是不能删除的

我们只需要找到尾cur,然后再标记尾的前一个节点pc,再连接pc和头节点即可,最后释放

//尾删 void push_front() { //如果只有一个节点 assert(Head->next != Head); //找尾 Node* cur = Head->prev; //标记 Node* pc = cur->prev; //重新连接 pc->next = Head; Head->prev = pc; //释放 delete cur; cur = nullptr; }

 效果展示:

头插

头插需要找到头结点的下一个节点,然后连接即可,例如:

注意:我们这里使用的是迭代器,因此在最后需要更新迭代器

//头插 iterator pop_back(iterator it, const T& date) { //先标记头节点的下一个节点 Node* pc = Head->next; //新节点 Node* cur = new Node(date); //连接 Head->next = cur; cur->prev = Head; cur->next = pc; pc->prev = cur; return it = it._node->prev; }
头删

头删还是先标记头节点的下一个节点cur,和cur的下一个节点,连接删除即可(注意更新迭代器)

//头删 iterator pop_front(iterator it) { //如果只有一个头节点 assert(Head->next != Head); //标记 Node* cur = Head->next; Node* pc = cur->next; //连接 Head->next = pc; pc->prev = Head; //释放 delete cur; cur = nullptr; return it._node = pc; }

效果展示:

 拷贝构造

(1)根据当前存在的链表拷贝构造出新链表,注意深浅拷贝:

如果是内置类型:那么使用字节拷贝

如果是自定义类型:那么使用浅拷贝(编译器默认的拷贝构造)

(2)调用拷贝构造函数时候,不会去再之前调用构造函数,这点很重要,关乎头结点

//拷贝构造 list(const list<T>& V) { //开辟头节点 Head = new Node; Head->next = Head; Head->prev = Head; list<T>::const_iterator it = V.begin(); while (it != V.end()) { //插入 push_back(*it); it++; } }

这里由于 V 的类型是 const 类型,需要支持 const 的迭代器

//begin() iterator begin()const { return Head->next; } //end() iterator end()const { return Head; }

效果展示:

赋值运算符重载

这里需要注意:空间的问题

我们还是采用新方法:拷贝构造出一个临时空间,再采用交换指针的方法

//交换 void swap(const list<int>& Vt) { //拷贝构造临时对象 list<int> Vs(Vt); //交换 std::swap(Vs.Head, Head); } //赋值运算符重载 list<T>& operator=(const list<int>& V) { swap(V); return *this; }

效果展示:

迭代器

迭代器的效果:如果是解引用,那么需拿到元素;如果是++,那么需要跳到下一个节点

因此:迭代器的指针不能是固定的,否则解引用拿到的就是节点而非数据(运算符重载)

我们先观察库里面迭代器的特点:

list<int>::iterator it = V.begin(); while (it != V.end()) { std::cout << *it << " "; it++; }
迭代器结构

这里 ++ 与 * 为了表示出不同的效果,我们使用的是运算符重载;对于非连续的空间的迭代             器实现,最好的方法是单独封装为一个类(大家记住即可),下面是迭代器的实现类结构

//迭代器 template<class T> struct __list_iterator { typedef __list_iterator<T> iterator; typedef list_node<T> Node; Node* _node; //构造 __list_iterator(Node* node) :_node(node) {} //解引用 T& operator*() { return _node->val; } //后置++ iterator& operator++(int) { iterator tmp(*this); _node = _node->next; return tmp; } //判断是否相等 bool operator!=(const iterator& it) { return _node != it._node; } };

注意:iterator 被重定义了,使用 iterator 其实就是调用 类模板 

begin()、end()

第一行会先执行 V.begin(),它的调用解析如下:

 过程解析:

先是调用 V.begin(),拿到节点,然后发生隐式转换,调用 iterator 类模板构造,参数是节          点,完成转换,返回值再交给 it ,同理 V.end()也是一样

这里的返回值是隐式类型转化,无法使用 & 

关系比较 !=

过程解析:

V.end() 作为参数先在类模板被初始化,然后直接调用运算符重载函数,返回 bool 值 

运算符重载 *

过程解析:

it 的类型是 iterator 类型,它的改变就是下面的 ++ 操作

 运算符重载 ++

为方便识别前置后置++:有 int 参数就是后置,无参数就是前置

无法修改的迭代器

对于无法修改的迭代器的效果:只可以访问,不能修改访问元素

方法(1)

const 的修饰效果:

所以我们可以直接在返回值这里加上 const 修饰,这是最直接的方法:

 方法(2)

我们发现 const T 跟 T 只是类型不同,其它都是相同的,那么我们可以通过类模板的参数去修改它

而 节点 的类型模板参数是 T ,所以我们得保证它跟迭代器的第一个参数是吻合的,比如:

不然节点的类型参数和迭代器的对不上,那就报错了

所以我们const类的迭代器和非const的迭代器,可以如下定义:

typedef list_iterator<T, T&> iterator; typedef list_iterator<T, const T&> const_iterator; 

 原因:我们的类模板重定义了,由于两个不同的类参数: <T  T&>     <T   const T&>

会形成两个不同的类:iterator<T  T&>    iterator<T   constT&>

所以我们只需要控制类模板的第二个参数去返回就可以达到“修改”和“不可修改”的效果了

const与非const迭代器转化图:

 这里最右边的第二个构造函数的参数我们现在只需要看就行了,这比较超前,我们后面再说!

总结:

对于迭代器const与非const的转化,以及其它类型的转化,我们都是调用构造函数来完成的,比如

一个节点类型  转化为  迭代器类型      一个迭代器  非const类型   转化为  const类型

这期间可能涉及到构造函数的重载,都是区分构造函数的参数来实现

Read more

当AI学会写“自传”:OpenClaw 的 SOUL.md 如何把配置文件变成一颗会变形的心

在多数软件的世界里,配置文件像一张表格:端口、路径、开关,冷静到几乎没有呼吸。但在 OpenClaw 的工作区里,有一份文件看起来像散文——它叫 SOUL.md。我在阅读你提供的材料时最强烈的感受是:它并不是“把模型调得更像某种语气”的小旋钮,而是一套更大胆的提案——用一份纯 Markdown 的自然语言文本,把代理(Agent)的身份、价值观、沟通风格与行为边界写成可阅读、可编辑、甚至可自我改写的“灵魂”。 官方模板那句“You’re not a chatbot. You’re becoming someone.”几乎像小说的开场白:这不再是“加载配置”,而更像“宣告存在”。 🧠 灵魂不是参数:SOUL.md 的定位是一份“存在论文档” 如果我把传统

By Ne0inhk
实测Gemini Pro:谷歌王牌AI,到底能帮我们解决多少实际问题?

实测Gemini Pro:谷歌王牌AI,到底能帮我们解决多少实际问题?

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一、核心亮点实测:不止是“多模态”,更是“真全能” * 1. 多模态处理:能“看、听、读、写”,还能“联动协作” * 2. 推理能力:复杂问题“会拆解、会纠错”,堪比专业助手 * 3. 代码能力:开发者的“全能帮手”,新手也能轻松上手 * 二、真实应用场景:这些领域,已经在用它提效了 * 1. 科研领域:帮研究员“节省时间”,专注核心工作 * 2. 内容创作:

By Ne0inhk
80+提示词 震撼发布|Seedance 2.0 提示词完全指南:从新手到“AI导演“

80+提示词 震撼发布|Seedance 2.0 提示词完全指南:从新手到“AI导演“

编者按 这两天,X.com、微博、小红书被一款名叫 Seedance 2.0 的 AI 视频生成模型刷屏。从 Tom Cruise 和 Brad Pitt 的"对打",到《复仇者联盟》的重制版,再到"水獭版"《老友记》……这些一度被认为需要好莱坞团队耗时数月才能完成的视频,如今只需一句提示词就能秒生成。 作为字节跳动推出的新一代多模态视频生成工具,Seedance 2.0 正式宣告:AI 视频创作时代已至,人人都可能成为"导演"。 今天,我们为你汇总了全网最实用的 Seedance 2.0 提示词和使用技巧,让你快速从入门到精通。

By Ne0inhk
LLM - Claude Code × OpenClaw:构建“左脑工程 + 右脑代理”的下一代 AI 开发工作流

LLM - Claude Code × OpenClaw:构建“左脑工程 + 右脑代理”的下一代 AI 开发工作流

文章目录 * 一、主题与读者定位 * 二、AI 助手的分化:从“一个模型”到“两个角色” * 三、Claude Code:工程化 Coding Agent 的典型形态 * 1)设计哲学:工程优先、可预测、深度聚焦 * 2)典型工作方式 * 3)记忆机制:项目级,而非人格级 * 4)应用示例 * 示例:微服务重构 * 四、OpenClaw:长期运行的个人 AI 代理 * 1)设计理念:持续在线 + 跨平台整合 * 2)关键能力:长期记忆 * 3)主动性:AI 开始“自己做事” * 4)

By Ne0inhk