【C++修炼秘籍】List深度剖析

【C++修炼秘籍】STL-List

☀️心有所向,日复一日,必有精进

☀️专栏《C++修炼秘籍》

☀️作者:早凉

☀️如果有错误,烦请指正,如有疑问可私信联系;


目录

【C++修炼秘籍】STL-List

文章目录

前言

一、list介绍

二、list的使用/接口介绍

构造函数

list iterator的使用

list capacity

list element access

list modifiers

三、list模拟实现

 结点定义

list类(构造函数和基本函数)

迭代器

增删查改 

迭代器失效问题

拷贝构造

 赋值运算符重载

size

有关类名和类型的问题

operator->

list和vector

总结


前言

相较于vector连续线性空间,list就显示的复杂很多,相较于vector,list既有优势也有缺点,两者并无好坏之分,只是各有其适用范围。首先,list的一个好处就是插入删除一个元素,就增加或释放一个元素空间,对空间的利用效率是极高的,对于任意位置的插入删除,list永远是O(1)的时间复杂度。

list和vector作为两个最常被使用的容器,什么时机去使用,也是一个比较复杂的问题,随着文章的深入,会慢慢对这个问题做出解答。


一、list介绍

//list node
template <class T>
struct __list_node {typedef void* void_pointer;void_pointer next;void_pointer prev;T data;
};

从标准模板库中我们观察链表结点,可以明显看出,list底层是带头双向循环链表,不支持随机访问

  • 可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
  • 双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
  • list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
  • 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
  • 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)

二、list的使用/接口介绍

构造函数

构造函数( (constructor))接口说明
list (size_type n, const value_type& val = value_type())构造的list中包含n个值为val的元素
list()构造空的list
list (const list& x)拷贝构造函数
list (InputIterator first, InputIterator last)用[first, last)区间中的元素构造list

list iterator的使用

函数声明接口说明
begin +
end
返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rbegin +
rend
返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的
reverse_iterator,即begin位置

迭代器就是一个类似于指针的行为,虽然底层实现不是原生指针。但是上层我们可以暂时理解如此。

list capacity

函数声明接口说明
empty检测list是否为空,是返回true,否则返回false
size返回list中有效节点的个数

list element access

函数声明接口说明
front返回list的第一个节点中值的引用
back返回list的最后一个节点中值的引用

list modifiers

函数声明接口说明
push_front在list首元素前插入值为val的元素
pop_front删除list中第一个元素
push_back在list尾部插入值为val的元素
pop_back删除list中最后一个元素
insert在list position 位置中插入值为val的元素
erase删除list position位置的元素
swap交换两个list中的元素
clear清空list中的有效元素

三、list模拟实现

 结点定义

这是模式实现的一个结点,其实我们使用class和struct均可,实现一个构造函数。 

    template<class T> struct list_node{T _val;list_node<T>*_prev;list_node<T>*_next;list_node(const T& x):_prev(nullptr),_next(nullptr),_val(x){}};

list类(构造函数和基本函数)

 先暂时实现一个list类运行起来,这里其实是使用__list_iterator<T>是一个类,封装迭代器,简单实现push_back,为下面的迭代器实现做准备。

template<class T>class list{public:typedef list_node<T> node;typedef __list_iterator<T> iterator;//typedef __list_const_iterator<T> const_iterator;list()//默认构造函数{_head = new node(T());_head->_next = _head;_head->_prev = _head;}template <class InputIterator>list(InputIterator first, InputIterator last)//迭代器构造函数{empty_initialize();while (first != last){push_back(*first);++first;}}iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}void push_back(T val){//新建结点node* NewNode = new node(val);node* tail = _head->_prev;tail->_next = NewNode;NewNode->_prev = tail;NewNode->_next = _head;_head->_prev = NewNode;}void insert(iterator pos, T val){}private:node*_head;};

迭代器

为什么这里要单独解析list的迭代器,在上文vector的迭代器我们使用的是原生指针,是因为vector是一块连续的线性空间,而list不保证结点空间连续,那么如何使得list的迭代器能像vector进行相同的上层操作,忽视底层细节,这是一个很大的问题。我们只有解决了迭代器问题才能解决模拟实现的大部分问题。

list<int>::iterator it = It.begin();while(it!=It.end())
{cout<<*it<<" ";++it;
}

 其实这个类里面就是一个结点的指针,能够指向结点,在使用C++的运算符重载,就能实现类似++的效果;

1、内嵌类型定义在类里面

2、像指针一样,所指像指针一样:解引用和++/--(像指针一样的行为)

 ❓为什么迭代器不能用<,其实vector和string可以使<,但是list迭代器不能使用小于?

                我们说了迭代器底层不是一个连续空间,虽然我们使用的++,--但是并不是表面上的样子;

这其实就是迭代器的价值,封装底层实现 ,让我们忽视底层细节,我们在使用其他STL库时,统一的访问形式降低了使用成本,迭代器默默承受了一切,但是当你实现迭代器你就发现,那有什么岁月静好,只是有人替你负重前行。

但是,我们学习要关注底层;接下来,让我们走进list迭代器的模拟实现!! 

template <class T>class __list_iterator//迭代器{typedef list_node<T> node;node* _pnode;public:__list_iterator(node* p):_pnode(p){}T& operator*(){return _pnode->_val;}bool operator != (__list_iterator<T>& it)//参数一定加引用{return it._pnode != _pnode;}void operator++()//这里实现有些问题其实是应该携带返回值的,返回*this;{_pnode =_pnode->_next;}};

❓但是还是存在一些问题,const迭代器呢?

(const迭代器和普通迭代器的区别就是不可通过*it去修改值。)

如果只是如下这样是解决不了问题的;

	// ++cit 这样的话,可以解引用,但是不能++/*const T& operator*() const{return _pnode->_data;}*/
    template <class T>class __list_const_iterator{typedef list_node<T> node;node* _pnode;public:__list_const_iterator(node* p):_pnode(p){}const T& operator*(){return _pnode->_val;}bool operator != (__list_const_iterator& it)//参数一定加引用{return it._pnode != _pnode;}void operator++()//这里实现有些问题其实是应该携带返回值的,返回*this;{_pnode = _pnode->_next;}};

 我们单独实现了const_iterator的一个类但是,大佬之所以为大佬,就是代码的复用,能写出来常人难以形容的代码;这里简单复现一下:

template <class T,class Ref>class __list_iterator//迭代器{typedef list_node<T> node;typedef __list_iterator<T, Ref> Self;node* _pnode;public:__list_iterator(node* p):_pnode(p){}/*T& operator*()//我这里自己写的就很迷,{return _pnode->_val;}*/Ref& operator*()//返回值不同不构成重载而且不是const迭代器这里传回的返回值也不是const{return _pnode->_val;}bool operator != (const Self& it)//参数一定加引用{return it._pnode != _pnode;}Self&  operator++(){_pnode =_pnode->_next;return *this;}Self& operator--(){_pnode = _pnode->_prev;return *this;}};

增删查改 

insert在list position 位置中插入值为val的元素
iterator insert(iterator pos, const T& val){node* NewNode = new node(val);node* cur = pos._pnode;node* pre = cur->_prev;pre->_next = NewNode;NewNode->_prev = pre;NewNode->_next = cur;cur->_prev = NewNode;return iterator(pos);}
erase删除list position位置的元素
iterator erase(iterator pos){assert(pos != end());node* prev = pos._pnode->_prev;node* next = pos._pnode->_next;prev->_next = next;next->_prev = prev;delete pos._pnode;return iterator(next);}
push_front在list首元素前插入值为val的元素
pop_front删除list中第一个元素
push_back在list尾部插入值为val的元素
pop_back删除list中最后一个元素
        void push_back(T val){//新建结点/*	node* NewNode = new node(val);node* tail = _head->_prev;tail->_next = NewNode;NewNode->_prev = tail;NewNode->_next = _head;_head->_prev = NewNode;*/insert(end(), val);}void pop_front(){erase(begin());}void pop_back(){erase(--end());}

 我们看到大多数代码均是复用出来的,所以代码复用真的很好,要学会“偷懒”。还是那句话

❗️代码复用真的好好 

迭代器失效问题

在vector中,insert和erase是失效的,但是在list迭代器中insert迭代器不失效;erase迭代器必然失效,空间结点都没了;

所以在erase后注意迭代器的使用;

拷贝构造

在拷贝构造前,先实现下析构函数,至于为什么,请看下文,实现拷贝构造去准备工作;

clear清空list中的有效元素
    void clear(){iterator it = begin();while (it != end()){it = erase(it);}}

其实这里就是我们要为什么先实现迭代器,实现了迭代器基本的模拟实现就完成了一半,还有一半,我认为是拷贝构造;

 析构函数

        ~list(){clear();delete _head;_head = nullptr;}

 ❓如果我们不写拷贝构造,那么默认的拷贝构造是否能用? 

 

报错了,如果把析构删掉,这段程序就通过了,所以要完成析构我们才能够正确的认识,这个默认的拷贝构造是一个浅拷贝,那就是同一块空间析构两次,大问题呀!

 这里实现一个比较牛的写法,第一次见,我看不懂,但大受震撼!

        void swap(list<T>& lt){std::swap(_head, lt._head);}list(const list<T>& it){empty_initialize();//这里如果不单独开辟一个空间就会出现问题list<T> tmp(it.begin(), it.end());//迭代器构造函数swap(tmp);}

 ❗️注意:empty_initialize();这个函数就是开辟一个头结点,这里如果不开头结点,交换给临时变量list,在析构时会默认存在头结点,所以会调用clear(),这时候对空访问会出才能问题。

 赋值运算符重载

	    list<T>& operator=(list<T> lt)//list& operator=(list lt) // 不建议{swap(lt);return *this;}

这真的很无敌,可能拷贝构造体现不出来,但是赋值运算符重载很无敌;

这里参数必须是传参而不是引用,直接调用拷贝构造;直接换;

size

size返回list中有效节点的个数

 size这个是比较麻烦的,如果你使用范围for进行遍历,一次可能效率还好,但是频繁size效率是让人难以忍受的;以空间换时间的方法,增加4个字节存储size;

这里还有一个问题,我们之前已经写了很多代码,如果加会不会很麻烦,实际上代码复用的好处就出现了,直接在复用的地方加就好;

有关类名和类型的问题

在类模板中,例如:list<T>,list为类名,list<T>为类型,构造函数名为list而不是list<T>;参数都是lsit<T>但是C++中有些坑,虽然C++中坑太多了

 ❓诶?诶?怎么,又要拿出那个图片了 

  在类中,类名就是类型,所以这样也没问题,但是不建议这么使用,我们只要知道有这个样子的事情就好了;

operator->

struct P
{int _row;int _col;P(int row = 0, int col = 0):_row(row), _col(col){}
};

 如果说我们想打印一个结构化数据,会出现一些问题,

最好的解决方法是重载-> 

很怪很怪

        T* operator->(){return &_pnode->_val;}

 其实就是it.operator->()返回结点数据的地址,就是一个p*,但是p*怎么访问,其实就是一个it->->_row就是it.operator()->_row,但是写两个箭头,很让人无语,所以编译器为了可读性,给我们做了特殊处理。

 ❓这里又又有问题,如果是const呢?

 

struct P
{int _row;int _col;P(int row = 0, int col = 0):_row(row), _col(col){}
};
void print_list(const my::list<P>& lt)
{my::list<P>::const_iterator it = lt.begin();while (it != lt.end()){it->_col++;cout << it->_col << ":" << it->_col << endl;++it;}cout << endl;
}
int main()
{my::list<P> lt;P p1(1, 1);lt.push_back(p1);lt.push_back(p1);lt.push_back(p1);lt.push_back(P(2, 2));lt.push_back(P(3, 3));my::list<P>::iterator it = lt.begin();while (it != lt.end()){cout << it->_col << ":" << it->_col << endl;//cout << (*it)._col <<":"<< (*it)._col << "";++it;}cout << endl;print_list(lt);
}

这个迭代器是有问题的呀,发现居然是能修改的;

所以可以再次修改迭代器的代码:

        typedef __list_iterator<T,T&,T*> iterator;typedef __list_iterator<T, const T&,const T*> const_iterator;
template <class T, class Ref, class Ptr>class __list_iterator//迭代器{public:typedef list_node<T> node;typedef __list_iterator<T, Ref, Ptr> Self;node* _pnode;__list_iterator(node* p):_pnode(p){}Ptr operator->(){return &_pnode->_val;}Ref& operator*(){return _pnode->_val;}bool operator != (const Self& it){return it._pnode != _pnode;}Self&  operator++(){_pnode =_pnode->_next;return *this;}Self& operator--(){_pnode = _pnode->_prev;return *this;}};

无敌, 类型typedef让我们修改很无敌,方便我们修改;

list和vector

vectorlist
底 层 结 构动态顺序表,一段连续空间带头结点的双向循环链表
随 机 访 问支持随机访问,访问某个元素效率O(1)不支持随机访问,访问某个元素
效率O(N)
插 入 和 删 除任意位置插入和删除效率低,需要移动元素,时间复杂
度为O(N),插入时有可能需要增容,增容:开辟新空
间,拷贝元素,释放旧空间,导致效率更低
任意位置插入和删除效率高,不
需要搬移元素,时间复杂度为
O(1)
空 间 利 用 率底层为连续空间,不容易造成内存碎片,空间利用率
高,缓存利用率高
底层节点动态开辟,小节点容易
造成内存碎片,空间利用率低,
缓存利用率低
迭 代 器原生指针对原生态指针(节点指针)进行封装
迭 代 器 失 效在插入元素时,要给所有的迭代器重新赋值,因为插入
元素有可能会导致重新扩容,致使原来迭代器失效,删
除时,当前迭代器需要重新赋值否则会失效
插入元素不会导致迭代器失效,
删除元素时,只会导致当前迭代
器失效,其他迭代器不受影响
使 用 场 景需要高效存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随
机访问

总结

好了List告一段落,所有细节都是难以,只有亲自实现才能够注意到;把握好不同容器的使用时机,才能够方便我们的使用。下期再见!!

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/427941.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

5G-A:“繁花”盛开在2024

2019年&#xff0c;我国正式发牌5G&#xff0c;开启5G商用新时代。通信技术十年一代&#xff0c;五年过去了&#xff0c;5G是否要进入“半代更迭”阶段&#xff1f; 2024年被视为5G-A商用元年&#xff0c;是5G走向6G的关键一跃。5G-A以R18为演进起点&#xff0c;在连接速率、网…

matlab appdesigner系列-容器2-选项卡组

所谓容器&#xff0c;就是可以放置常有组件的空间&#xff0c;类似一种画布。默认背景画布就是一张画布&#xff0c;选项卡组&#xff0c;可以同时增加好几个画布空间&#xff0c;以满足不的组件放置需求。 1&#xff09;将选项卡组拖拽到画布上 2&#xff09;将选项卡组拉大背…

CSS3基础知识总结

目录 一、CSS3 边框 1.border-radius&#xff1a;圆角边框 2.box-shadow&#xff1a;添加阴影 3.border-image&#xff1a;图片边框 二、CSS3 渐变 1.线性渐变(Linear Gradients) a.由上到下&#xff08;默认&#xff09; b.从左到右 c.对角 d.使用角度 2.径向渐变(…

HTTP与HTTPS的工作流程

HTTP与HTTPS的工作流程 http知识点回顾1、HTTP访问的过程2、HTTP常见状态码3、HTTP 协议一共五大特点 https的工作流程1、对称加密2、非对称加密3、https工作流程 http知识点回顾 1、HTTP访问的过程 &#xff08;1&#xff09;解析url&#xff0c;获取 url 中包含的域名&…

将AWS iot消息数据发送Kinesis Firehose Stream存向S3

观看此文章之前&#xff0c;请先学习AWS iot的数据收集&#xff1a; 使用Linux SDK客户端向AWS Iot发送数据-CSDN博客 1、工作原理&#xff1a; 1.1 规则 规则可让您的设备与 AWS 服务进行交互。分析规则并根据物品发送的消息执行操作。您可以使用规则来支持任务&#xff0…

sky_take_out

day01&#xff1a; 前端网址通过nginx访问后端网址&#xff08;前后网址不一致&#xff09;&#xff0c;有三个好处&#xff1a; 一是提高访问速度&#xff0c;二是进行负载均衡&#xff0c;三是保障后端安全性 用md5加密了密码 后端使用knife4j调试,用Swagger生成接口文档&am…

Java基础进阶02-xml

目录 一、XML&#xff08;可拓展标记语言&#xff09; 1.学习网站&#xff1a; 2.作用 3.XML标签 4.XML语法 5.解析XML &#xff08;1&#xff09;常见解析思想DOM 6.常见的解析工具 7.DOM4j的使用 8.文档约束 &#xff08;1&#xff09;概述 &#xff08;2&#xf…

【经验分享】豆瓣小组的文章/帖子怎么删除?

#豆瓣小组的文章/帖子怎么删除&#xff1f;# 第一步&#xff1a; 手机登录豆瓣app ↓ 点右下角“我” ↓ 然后在页面点击我的小组 ↓ 点我发布的 ↓ ↓ 再任意点开一个帖子 ↓ 在文章和帖子的右上角有一个笔状的图标&#xff0c;切记不是右上角的横三点… ↓ ↓ 最后点下边的…

[docker] Docker资源管理

一、docker资源控制 Docker通过Cgroup 来控制容器使用的资源配额&#xff0c;包括CPU、内存、磁盘三大方面&#xff0c;基本覆盖了常见的资源配额和使用量控制。Caroup 是ControlGroups的缩写&#xff0c;是Linux 内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如…

多线程编程漫谈

文章目录 如何复用线程&#xff1f;多线程编程常用的设计模式Future 模式生产者 - 消费者模式 小结 多线程是很多人在提升技术能力的过程中遇到的第一个坎&#xff0c;关于这部分的资料在网络上已经很多了&#xff0c;但是这些资料往往只重知识点的输出&#xff0c;很少和实际的…

洛谷刷题-【入门2】分支结构

目录 1.苹果和虫子 题目描述 输入格式 输出格式 输入输出样例 2.数的性质 题目描述 输入格式 输出格式 输入输出样例 3.闰年判断 题目描述 输入格式 输出格式 输入输出样例 4.apples 题目描述 输入格式 输出格式 输入输出样例 5.洛谷团队系统 题目描述 …

【原创】linux为什么不是实时操作系统

文章目录 一、什么是实时操作系统&#xff08;RTOS&#xff09;&#xff1f;二、linux为什么不是实时操作系统&#xff1f;中断响应时间中断处理时间任务调度时间1、No Forced Preemption(Server)2、Voluntary Kernel Preemption(Desktop)3、Preemptible Kernel(Low-Latency De…