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

这篇具有很好参考价值的文章主要介绍了【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、像指针一样,所指像指针一样:解引用和++/--(像指针一样的行为)

 【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl❓为什么迭代器不能用<,其实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;
		}
	};

【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl❓但是还是存在一些问题,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;
		}

 【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl❓如果我们不写拷贝构造,那么默认的拷贝构造是否能用? 

 【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl

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

 这里实现一个比较牛的写法,第一次见,我看不懂,但大受震撼!【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl

        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++中坑太多了

【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl

 【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl❓诶?诶?怎么,又要拿出那个图片了 【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl

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

operator->

struct P
{
	int _row;
	int _col;

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

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

【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl

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

很怪很怪

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

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

 【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl❓这里又又有问题,如果是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

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

总结

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

【C++修炼秘籍】List深度剖析,c++,list,链表,数据结构,stl 文章来源地址https://www.toymoban.com/news/detail-824048.html

到了这里,关于【C++修炼秘籍】List深度剖析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++入门之stl六大组件--List源码深度剖析及模拟实现

    文章目录 前言 一、List源码阅读 二、List常用接口模拟实现 1.定义一个list节点 2.实现一个迭代器 2.2const迭代器 3.定义一个链表,以及实现链表的常用接口 三、List和Vector 总结 本文中出现的模拟实现经过本地vs测试无误,文件已上传gitee,地址:list: 模仿实现stl的list - Gitee.com 首

    2024年02月13日
    浏览(51)
  • 【C++修炼之路】list 模拟实现

    👑作者主页:@安 度 因 🏠学习社区:StackFrame 📖专栏链接:C++修炼之路

    2024年02月16日
    浏览(38)
  • Redis数据结构——链表list

    链表是一种常用的数据结构,提供了顺序访问的方式,而且高效地增删操作。 Redis中广泛使用了链表,例如:列表的底层实现之一就是链表。 在Redis中,链表分为两部分:链表信息 + 链表节点。 链表节点用来表示链表中的一个节点,基础的值和指向前和后的指针 链表信息,

    2024年02月13日
    浏览(38)
  • 数据结构与算法 | 链表(Linked List)

    链表(Linked List)是一种线性数据结构,它由一系列节点(Node)组成,每个节点包含两部分:数据和指向下(上)一个节点的引用(或指针)。链表中的节点按照线性顺序连接在一起(相邻节点不需要存储在连续内存位置),不像数组一样存储在连续的内存位置。链表通常由

    2024年02月08日
    浏览(49)
  • 数据结构之List(双向链表)的实现

    方法名 参数 功能 返回 find const T val, int n, listNode * p 区间查找 从p往前数n个节点 指针或NULL const T val, listNode * p 区间查找 从p往前到首节点 const T val 查找 Size void 链表规模 size empty void 判空 bool first void 返回首节点 首节点指针 clear void 清空链表 void insertAsFirst const T val 作为首节

    2024年02月16日
    浏览(46)
  • 数据结构英文习题解析-第二章 链表List

    前言:最近快到FDS考试了,po重刷了一下学校的题目,自己整理了一些解析orz 因为po在自己找解析和学习的过程中非常痛苦,所以在此共享一下我的题目和自己写的解题思路,欢迎各位指出错误~全章节预计会陆续更新,可在专栏查看~ HW2 1. For a sequentially stored linear list of leng

    2024年04月11日
    浏览(53)
  • C++之链表list

    1.1 list的基本特性         C++的list是一种双链表,这意味着每个元素都有一个指向前一个和后一个元素的指针。相比于单链表,双链表在插入和删除操作中更为高效,因为它不需要遍历链表来找到插入或删除的位置。         list支持常见的序列操作,如添加元素(p

    2024年02月04日
    浏览(33)
  • 【数据结构】_4.List接口实现类LinkedList与链表

    目录 1.链表的结构与特点 1.1 链表的结构: 1.2 链表的特点: 2. 不带头单链表的模拟实现 3. 单链表OJ 3.1 题目1:移除链表元素:  3.2 题目2:反转一个单链表 3.3 题目3:返回链表的中间结点 3.4 题目4:链表的倒数第k个结点 3.5 题目5:合并两个有序链表 3.6 题目6:链表的回文结构

    2024年02月15日
    浏览(44)
  • 【STL源码分析】c++,List双向链表源码分析。自己实现list双向链表。

    参考链接:https://blog.csdn.net/man_sion/article/details/71003095? 先抽取要实现的功能,由于迭代器有些麻烦,就不使用了。要实现的功能有,push_back,pop_back,insert(指定位置,指定值),insert(指定位置,list,区间值),reverse,clear,getsize,begin,end,构造和析构函数,empty。 相关力扣题目:设计

    2024年02月03日
    浏览(46)
  • C++ list链表模拟实现

    目录 前言: 模拟实现: 迭代器的实现: list类功能函数实现:  初始化成空函数(empty_init): 构造函数:  拷贝构造函数: 尾插(push_back): 插入(insert): 删除(erase):  尾删(pop_back): 头插(push_front): 头删(pop_front):  清理(clear):  交换(swap): 赋值重载:

    2024年04月12日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包