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

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

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

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

☀️作者:早凉

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


目录

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

文章目录

前言

一、list介绍

二、list的使用/接口介绍

构造函数

[list iterator的使用](#list iterator的使用)

[list capacity](#list capacity)

[list element access](#list element access)

[list modifiers](#list modifiers)

三、list模拟实现

结点定义

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

迭代器

增删查改

迭代器失效问题

拷贝构造

赋值运算符重载

size

有关类名和类型的问题

operator->

list和vector

总结


前言

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

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


一、list介绍

cpp 复制代码
//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均可,实现一个构造函数。

cpp 复制代码
    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,为下面的迭代器实现做准备。

cpp 复制代码
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进行相同的上层操作,忽视底层细节,这是一个很大的问题。我们只有解决了迭代器问题才能解决模拟实现的大部分问题。

cpp 复制代码
list<int>::iterator it = It.begin();

while(it!=It.end())
{
	cout<<*it<<" ";
	++it;
}

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

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

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

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

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

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

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

cpp 复制代码
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去修改值。)

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

cpp 复制代码
	// ++cit 这样的话,可以解引用,但是不能++
		/*const T& operator*() const
		{
		return _pnode->_data;
		}*/
cpp 复制代码
    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的一个类但是,大佬之所以为大佬,就是代码的复用,能写出来常人难以形容的代码;这里简单复现一下:

cpp 复制代码
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的元素 |

cpp 复制代码
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位置的元素 |

cpp 复制代码
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中最后一个元素 |

cpp 复制代码
        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中的有效元素 |

cpp 复制代码
    void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

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

析构函数

cpp 复制代码
        ~list()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

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

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

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

cpp 复制代码
        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(),这时候对空访问会出才能问题。

赋值运算符重载

cpp 复制代码
	    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->

cpp 复制代码
struct P
{
	int _row;
	int _col;

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

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

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

很怪很怪

cpp 复制代码
        T* operator->()
		{
			return &_pnode->_val;
		}

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

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

cpp 复制代码
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);
}

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

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

cpp 复制代码
        typedef __list_iterator<T,T&,T*> iterator;
		typedef __list_iterator<T, const T&,const T*> const_iterator;
cpp 复制代码
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告一段落,所有细节都是难以,只有亲自实现才能够注意到;把握好不同容器的使用时机,才能够方便我们的使用。下期再见!!

相关推荐
Leo.yuan7 分钟前
39页PDF | 华为数据架构建设交流材料(限免下载)
数据结构·华为
小乖兽技术18 分钟前
C#与C++交互开发系列(二十):跨进程通信之共享内存(Shared Memory)
c++·c#·交互·ipc
半夜不咋不困21 分钟前
单链表OJ题(3):合并两个有序链表、链表分割、链表的回文结构
数据结构·链表
王俊山IT23 分钟前
C++学习笔记----10、模块、头文件及各种主题(二)---- 预处理指令
开发语言·c++·笔记·学习
幼儿园园霸柒柒43 分钟前
第七章: 7.3求一个3*3的整型矩阵对角线元素之和
c语言·c++·算法·矩阵·c#·1024程序员节
2401_858286111 小时前
C6.【C++ Cont】cout的格式输出
开发语言·c++
忘梓.1 小时前
排序的秘密(1)——排序简介以及插入排序
数据结构·c++·算法·排序算法
zhj186791306131 小时前
远程控制项目第四天 功能实现
c++
摆烂小白敲代码2 小时前
背包九讲——背包问题求方案数
c语言·c++·算法·背包问题·背包问题求方案数
小柯J桑_2 小时前
C++:set详解
c++·set