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

探索解析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类型

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

相关文章:

  • 零陵做网站快速排名优化seo
  • 购物网站怎么建立百度推广登录入口
  • 西安大型网站设计公司郑州网络推广效果
  • 漳州软件开发公司优化关键词方法
  • 个人可以建设新闻网站吗站长工具在线平台
  • 学做网站的基本网络营销与直播电商就业前景
  • Linux学习笔记:PCIe内核篇(3):DPC服务
  • 浪潮存储单卷单LUN不能超过64T、128T解决方法
  • 领域驱动设计(DDD)【13】之重构中的坏味道:深入理解依恋特性(Feature Envy)与表意接口模式
  • 深入浅出:RocketMQ与Kafka的双剑合璧,实现高可用与高吞吐
  • 计算机网络-----详解HTTP协议
  • 用Python做一个手机镜头
  • GitHub中openmmlab和Detectron2的区别
  • 打造灵活强大的PDF解析管道:从文本提取到智能分块的全流程实战
  • Java编程中的单例模式
  • 微信点餐小程序—美食物
  • python学智能算法(十六)|机器学习支持向量机简单示例
  • ETH节点各个服务应用的作用
  • HarmonyOS5 折叠屏适配测试:验证APP在展开/折叠状态下的界面自适应,以及会出现的问题
  • 【sgMoreMenu】自定义组件:“更多操作⌵”上下文关联按钮)下拉菜单。可用于表格操作列显示不下折叠的更多操作下拉框。
  • WeakAuras Lua Script [ICC BOSS 12 - The Lich King]
  • Transformer中核心机制的讲解:位置编码,注意力机制优化!
  • 【Python报错】成功解决error: subprocess-exited-with-error:安装lxml模块不再报错
  • 中宇厨卫启动年中品质回馈活动,深化用户体验
  • 京东正式开源 Taro on HarmonyOS C-API 版本,为鸿蒙应用跨端开发提供高性能框架
  • 阿里云Web应用防火墙3.0使用CNAME接入传统负载均衡CLB