C++基础入门:深挖list的那些事

◆博主名称:少司府

欢迎来到少司府的博客☆*: .。. o(≧▽≦)o .。.:*☆

数据结构系列个人专栏:

初阶数据结构_少司府的博客-CSDN博客

C++基础个人专栏:

C++初阶_少司府的博客-CSDN博客

⭐琢玉成器终有时,笔底生花夺锦归

目录

一、list基础相关

[1.1 标准库里的list](#1.1 标准库里的list)

[1.2 迭代器访问与push_back、push_front](#1.2 迭代器访问与push_back、push_front)

[1.3 emplace_back()](#1.3 emplace_back())

[1.4 insert()与find()](#1.4 insert()与find())

[1.5 reverse()与sort()](#1.5 reverse()与sort())

[1.6 unique()去重、splice()转移](#1.6 unique()去重、splice()转移)

二、list模拟实现

[2.1 单节点的模拟实现](#2.1 单节点的模拟实现)

[2.2 iterator](#2.2 iterator)

[2.3 list](#2.3 list)

[2.3.1 框架](#2.3.1 框架)

[2.3.2 begin()和end()](#2.3.2 begin()和end())

[2.3.3 默认构造和拷贝构造](#2.3.3 默认构造和拷贝构造)

[2.3.4 =重载和析构](#2.3.4 =重载和析构)

[2.3.5 插入删除](#2.3.5 插入删除)

三、list与vector的对比


一、list基础相关

1.1 标准库里的list

list的文档介绍

如图,我们点击链接就可以跳转到标准文档中查看list的相关介绍。

list是一个序列容器,它允许恒定时间内的插入删除 ,可以在任意的序列位置,并且支持迭代器。

1.2 迭代器访问与push_back、push_front

如图,list的迭代器的用法和vector、string没什么区别。

但是,需要注意的是:

list的迭代器是双向迭代器,前面我们学的vector和string的迭代器是随机迭代器,forward_list(单链表)的迭代器是单向迭代器,而后面要学的stack和queue没有迭代器,他们本身也是属于空间配置器的范畴。

cpp 复制代码
sort(lt.begin(), lt.end()); // error,不支持,要求随机迭代器,使用不匹配的迭代器会报错

如图,这行代码会报错,原因就是sort(排序算法,底层是自省排序)要求的是随机迭代器。传入双向迭代器属于"缩小了范围"。

cpp 复制代码
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_front(3);
	lt.push_front(4);

	list<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << ' ';
		it++;
	}
	cout << endl;
	for (auto e : lt) cout << e << ' ';
	cout << endl;

我们来看这段代码,push_back、push_front分别是尾插和头插,且,list支持迭代器也支持范围for。

结果如图,我们可以看到,sort排序默认按照从小到大排。

要用到sort,我们需要algorithm的算法头文件。

1.3 emplace_back()

emplace_back的具体用法和push_back类似,都是尾插,都支持传类型。

cpp 复制代码
	list<int> lt;
	lt.emplace_back(1);
	lt.emplace_back(2);
	lt.emplace_back(3);
	lt.emplace_back(4);
	for (auto e : lt) cout << e << ' '; // 与push_back类似
	cout << endl;

	list<A> lt1;
	A aa1(1, 1);
	lt1.push_back(aa1);
	lt1.push_back(A(2, 2));

	lt1.emplace_back(aa1);
	lt1.emplace_back(A(2, 2));
	cout << endl;
	// 支持直接传构造A对象的参数emplace_back
	lt1.emplace_back(3, 3); // 前面是构造+拷贝构造,这里相当于直接构造

有所区别的是:empalce_back支持直接传对象的参数。

结果如图,在push_back的时候,一直是构造+拷贝构造,但是emplace_back直接传对象参数的话相当于直接构造。

1.4 insert()与find()

如图,insert支持在迭代器位置插入删除 ,find查找函数属于std算法库的,如果没找到目标值x就返回第二个迭代器参数。

1.5 reverse()与sort()

如图,list中有自己的reverse逆置,也可以调用算法库里面的逆置,算法库里面的reverse需要传入迭代器区间参数。

cpp 复制代码
lt.sort(greater<int>());

sort默认是升序排序的,如果想要降序,需要传入一个仿函数。

1.6 unique()去重、splice()转移
cpp 复制代码
	list<int> lt;
	lt.push_back(5);
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(4);

	lt.sort();
	lt.unique(); // 去重,要求链表是有序的
	for (auto e : lt) cout << e << ' ';
	cout << endl;

如图,list中的unique函数去重要求链表是有序的。

结果如图,去重删去了多余的4。

splice使用的两种场景:

1)、把一个链表的节点转移到另一个链表

cpp 复制代码
	// 把一个链表的节点转移到另一个链表
	std::list<int> mylist1, mylist2;
	std::list<int>::iterator it;

	// set some initial values:
	for (int i = 1; i <= 4; ++i)
		mylist1.push_back(i);      // mylist1: 1 2 3 4

	for (int i = 1; i <= 3; ++i)
		mylist2.push_back(i * 10);   // mylist2: 10 20 30

	it = mylist1.begin();
	++it;                         // points to 2

	mylist1.splice(it, mylist2);  

如图,mylist1: 1 10 20 30 2 3 4,剪切转移,mylist2的值转移到mylist1中2的前面,之后mylist2就空了。

2)、移动当前链表本身的节点

cpp 复制代码
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(6);
	for (auto e : lt) cout << e << ' ';
	cout << endl;

	int x;
	cin >> x;
	auto i = find(lt.begin(), lt.end(), x);
	lt.splice(lt.begin(), lt, i, lt.end()); 
	for (auto e : lt) cout << e << ' ';
	cout << endl;

如图,find找到x返回相应位置的迭代器,splice截取x及其之后的链表数据并将其头插到链表lt中。

二、list模拟实现

2.1 单节点的模拟实现
cpp 复制代码
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T& data = T())
			:_data(data)
			,_next(nullptr)
			,_prev(nullptr)
		{ }
	};

如图,我们用一个类模板来模拟实现单节点list_node,双向链表list的node节点需要前驱指针prev和指向下一个节点的指针next,自己实现默认构造。

如图,库里面的node节点的细节封装在_List_node_base类中。

2.2 iterator

在实现迭代器 的时候,我们会发现普通迭代器和const迭代器有很多功能是相同的,那么我们是否可以只实现一个,另一个交给编译器复用代码呢?答案是肯定的。

我们可以通过模板来实现这样一个功能:

cpp 复制代码
template<class T,class Ref,class Ptr>
struct list_iterator
{
	typedef list_node<T> Node;
	typedef list_iterator<T,Ref,Ptr> Self;
	Node* _node;

	list_iterator(Node* node)
		:_node(node)
	{ }

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	Self operator++(int)
	{
		Self tmp(*this);
		_node = _node->_next;

		return tmp;
	}

	Self operator--(int)
	{
		Self tmp(*this);
		_node = _node->_prev;

		return tmp;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

	bool operator==(const Self& s)
	{
		return !(*this != s);
	}
};

如图,模板参数中T是类型;Ref是T类型的引用,在实例化时可以传普通T引用,也可以是const T引用;

Ptr是T的指针,传参和引用一样。

例如:

cpp 复制代码
typedef list_iterator<T,T&,T*> iterator; // 用模板来实现普通iterator和const iterator两种类
typedef list_iterator<T,const T&,const T*> const_iterator;

这样,我们就实现了iterator和const_iterator两个迭代器。

2.3 list
2.3.1 框架
cpp 复制代码
template<class T>
class list
{
	typedef list_node<T> Node;
public:
	//typedef list_iterator<T> iterator;
	//typedef const_list_iterator<T> const_iterator;

	typedef list_iterator<T,T&,T*> iterator; // 用模板来实现普通iterator和const iterator两种类
	typedef list_iterator<T,const T&,const T*> const_iterator;
private:
	Node* _head;
	size_t _size;
};

如图,成员变量就只有哨兵位头节点的指针和数据个数_size。

我们将迭代器typedef一下方便书写。

2.3.2 begin()和end()
cpp 复制代码
	iterator begin()
	{
		// return iterator(_head->_next);
		return _head->_next; // 隐式类型转换
	}

	iterator end()
	{
		// return iterator(_head);
		return _head;
	}

	const_iterator begin() const
	{
		return _head->_next; 
	}

	const_iterator end() const
	{
		return _head;
	}

如图,我们实现普通版本和const版本的begin和end。

可以直接返回_head->_next,发生隐式类型转换返回iterator类型。

2.3.3 默认构造和拷贝构造
cpp 复制代码
	void empty_init()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
		_size = 0;
	}

	list()
	{
		empty_init();
	}

如图,默认构造和拷贝构造都需要初始化链表,因此我们先封装一个接口empty_init

申请一个哨兵位的头节点,并且自己指向自己。

cpp 复制代码
	list(initializer_list<int> il)
	{
		empty_init();
		for (auto& e : il)
		{
			push_back(e);
		}
	}

	list(const list<T>& lt)
	{
		empty_init();
		for (auto& e : lt)
		{
			push_back(e);
		}
	}

如图,先初始化链表,再利用范围for遍历尾插到链表中。

其中,initializer_list 是C++11引入的初始化列表代理类 ,内部保存首元素指针+元素长度,核心作用是让容器、自定义类可以直接使用 {} 批量初始化。

2.3.4 =重载和析构
cpp 复制代码
list<T>& operator=(list<T> lt)
{
	swap(lt);
	return *this;
}

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

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

void swap(list<T>& lt)
{
	std::swap(_head, lt._head);
	std::swap(_size, lt._size);
}

= 的重载方式和之前一样,调用swap完成,= 的参数 list<T> lt 在传入是编译器会生成临时对象,实际上传入的是实参的拷贝,swap交换之后析构的是临时对象的资源,不需要自己额外写tmp。

2.3.5 插入删除
cpp 复制代码
	void push_back(const T& x)
	{
		//Node* newnode = new Node(x);
		//Node* tail = _head->_prev;

		//tail->_next = newnode;
		//newnode->_prev = tail;
		//newnode->_next = _head;
		//_head->_prev = newnode;
		//_size++;

		insert(end(), x);
	}

	iterator insert(iterator pos, const T& x)
	{
		Node* cur = pos._node;
		Node* prev = cur->_prev;

		Node* newnode = new Node(x);

		newnode->_next = cur;
		cur->_prev = newnode;
		newnode->_prev = prev;
		prev->_next = newnode;

		_size++;
		return newnode;
	}

	iterator erase(iterator pos)
	{
		assert(pos != end());

		Node* prev = pos._node->_prev;
		Node* next = pos._node->_next;

		prev->_next = next;
		next->_prev = prev;
		delete pos._node;

		_size--;
		return next;
	}

	void pop_back()
	{
		erase(--end());
	}

	void pop_front()
	{
		erase(begin());
	}

	void push_front(const T& x)
	{
		insert(begin(), x);
	}

	size_t size() const
	{
		return _size;
	}

	bool empty() const
	{
		return _size == 0;
	}

如图,push_back可以直接复用insert,插入删除的逻辑在数据结构部分就讲了,这里不再过多赘述。具体看这里:点击查看双向链表章节

三、list与vector的对比

vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同。

|------------|----------------------------------------------------------|--------------------------------|
| | vector | list |
| 底层存储结构 | vector底层是一块连续的空间,缓存命中率高,支持下标的随机访问 | list底层空间不连续,缓存命中率低,不支持下标的随机访问。 |
| 插入删除 | 插入删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,拷贝元素释放旧空间开辟新空间效率更低。 | 效率较高,只需要申请新节点或销毁该节点,再改变指针指向。 |
| 迭代器 | 调用原生迭代器指针 | 自己封装实现迭代器 |
| 使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入删除,不支持下标随机访问 |

本期的分享就到这里,如果觉得博主的文章比较对胃口的话,可以点一个小小的关注~

您的三连是我持续更新的动力~

相关推荐
罗超驿3 小时前
14.MySQL索引底层原理:从数据结构到B+树的深度解析
数据结构·b树·mysql
孬甭_3 小时前
单链表详解
c语言·数据结构
XMYX-03 小时前
30 - Go 随机数与 UUID 生成:原理、陷阱与工程实践
开发语言·golang
东北甜妹3 小时前
K8s etdc备份恢复 和 集群升级 证书更新
云原生·容器·kubernetes
xiaoye-duck3 小时前
Qt 初识核心:从 HelloWorld 到基础控件,吃透对象树与内存管理
开发语言·qt
我的xiaodoujiao3 小时前
API 接口自动化测试详细图文教程学习系列19--添加封装其他的方法
开发语言·python·学习·测试工具·pytest
Co_Hui3 小时前
Java: 集合
java·开发语言
ch.ju3 小时前
Java程序设计(第3版)第四章——动态部分
java·开发语言
诙_3 小时前
C++学习总结
开发语言·c++·学习