【C++】STL容器之list

前言

C++引入了面向对象的思想,相比于C语言,一个类能更好地对一些数据结构进行管理和操作。

在C语言中,我们动态开辟一个个的节点,并且用指针将他们连接起来,形成链式结构,链式结构在物理上不连续,在逻辑上连续

在C++中,基于面向对象的思想,用来管理这链式结构的便应运而生,从本质上讲,list是带头双向循环链表
在这里插入图片描述

目录

1.list的简介

我们学习STL时,文档是我们的利器,学会查文档会让学习事半功倍,以下是两个C++文档网站:

list的文档介绍:

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

STL作为泛型编程的典范,我们的list类自然就是一个类模板

template <class T>
	class list {
    
    
	//...
	}

模板参数T很显然,就是我们想要在节点里插入的元素的类型,可以是int, char等内置类型数据,也可以是string, vector等自定义类型数据

list <int> lt1;
list <char> lt2;
list <vector<int>> lt3;
list <string> lt4;

再来看一下list类的成员变量

Node* _head;------>节点指针

节点是一个结构体,list用一个结构体指针来维护整张链表,相信大家对于节点并不陌生

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

在这里,节点也是用类模板定义出来的,因为要和list的模板参数T

我们typedef了一下

typedef __list_node<T> Node;

2.vector的常见接口及模拟实现

2.1 list类对象获取元素和迭代器的接口

和string、vector的迭代器是原生指针不同,由于list的每个节点的物理地址不连续,所以list的迭代器不能用原生指针来替代

于是我们创建了一个类iterator,将解引用、++等操作进行了类的运算符重载,从表观的调用上达到了和原生指针相同的效果

由于迭代器分为普通迭代器和const迭代器,参照STL源码,我们将迭代器设计成带有3个模板参数的一个类

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<T, Ref, Ptr>(Node* node)
		: _node(node) {
    
    }

	Ref operator*() {
    
    
		return _node->_data;
	}
	self operator++() {
    
    
		_node = _node->_next;
		return __list_iterator(_node);
	}
	self operator++(int) {
    
    
		__list_iterator<T, Ref, Ptr> tmp(_node);
		_node = _node->_next;
		return tmp;
	}
	self operator--() {
    
    
		_node = _node->_prev;
		return __list_iterator(_node);
	}
	self operator--(int) {
    
    
		__list_iterator<T, Ref, Ptr> tmp(_node);
		_node = _node->_prev;
		return tmp;
	}
	bool operator!=(const self& it2) {
    
    
		return (_node != it2._node);
	}
	bool operator==(const self& it2) {
    
    
		return (_node == it2._node);
	}
	Ptr operator->() {
    
     //In order to get structures' member if T is struct type
		return &_node->_data;
	}
};

这样一来,解引用一个迭代器,虽然表面上是解引用了一个类,但是运算符重载告诉我们实际上解引用拿到了iterator类的成员_node的_data

其他操作同样道理,注意的是,list带头结点,也就是哨兵位

接口名称 接口作用
begin() 返回头节点的下一个节点的迭代器
end() 返回头节点的迭代器

接口的模拟实现:

//Iterator/
iterator begin() {
    
    
	return iterator(_head->_next);
}
iterator end() {
    
    
	return iterator(_head);
}
const_iterator begin() const{
    
     
	return const_iterator(_head->_next);
}
const_iterator end() const{
    
    
	return const_iterator(_head);
}

2.2 list类对象的常见构造

函数名 功能
list 无参构造
list(int n, const T& x = T()) 构造并初始化n个x(x的缺省值为0)
template <class InputInerator> list(InputInerator first, InputInerator last) 使用迭代器进行初始化构造
list(const vector&v) 拷贝构造函数
~list() 析构函数
operator= 赋值重载,将一个list对象赋值给另一个list对象

多种构造函数的使用:

void TestList()
{
    
    
	std::list<int> first; // 无参构造
	std::list<int> second (4,100); //构造并初始化4个100
	std::list<int> third (second.begin(),second.end()); // 用second的迭代器区间构造
	std::list<int> fourth (third); // 拷贝构造
}

接口的模拟实现:

  • 构造函数
//无参默认构造函数
list() 
	:_head(new Node)
{
    
    
	_head->_next = _head;
	_head->_prev = _head;
	_head->_data = T();
}
//构造并初始化n个x
list(int n, const T& x = T()) 
	:_head(new Node) {
    
    
	_head->_next = _head;
	_head->_prev = _head;
	_head->_data = T();
	Node* cur = _head;
	for(size_t i  = 0; i < (size_t)n; i++) {
    
    
		Node* prev = cur;
		Node* next = cur->_next;
		Node* newnode = new Node(x);
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = next;
		next->_prev = newnode;
		cur = cur->_next;
	}
}
//用迭代器区间构造
template <class InputIterator>
list(InputIterator first, InputIterator last)
	:_head(new Node)
{
    
    
	_head->_next = _head;
	_head->_prev = _head;
	_head->_data = T();
	while(first != last) {
    
    
		push_back(*first);
		first++;
	}
}
  • 拷贝构造
list(const list<T>& lt) 
	:_head(new Node)
{
    
    
	_head->_next = _head;
	_head->_prev = _head;
	_head->_data = T();
	for(auto e: lt) {
    
    
		push_back(e);
	}
}
  • 析构函数
//析构函数
~list() {
    
    
	clear(); //复用的clear函数在下面modify里面讲
	delete _head;
	_head = nullptr;
}
  • 赋值重载
//赋值重载
vector& operator=(vector v) {
    
    
    swap(v);
    return *this;
}

2.3 list类对象的容量操作

接口名称 接口作用
size() 返回数组中元素个数
empty() 判断数组是否为空数组

接口的模拟实现:

  • size()
size_t size() {
    
    
	size_t ret = 0;
	Node* cur = _head;
	while (cur->_next != _head) {
    
    
		cur = cur->_next;
		ret++;
	}
	return ret;
}
  • empty()
// 判空
bool empty() {
    
    
	return _head->_next == _head;
}

2.4 list类对象获取/修改元素接口

接口名称 接口作用
front() 获取第一个有效节点元素的值
back() 获取最后一个有效节点元素的值
iterator insert(iterator pos, const T& x) 在pos迭代器之前插入1个元素为x的节点
push_front(const T& val) 头插1个元素为val的节点
push_back(const T& val) 尾插1个元素为val的节点
erase(iterator pos) 删除pos迭代器对应的节点
erase(iterator first, iterator last) 删除迭代器左闭右开区间内的节点
pop_front() 头删一个节点
pop_back() 尾删一个节点
clear() 清除所有元素并将size置为0
swap() 交换两个类对象

注意下insert和erase的返回值

  • insert的插入是在pos前面插,返回的是被插的节点的迭代器
  • erase删完之后返回被删的最后一个节点的下一个位置的迭代器
  • 还有用erase删完有效节点后如果再去删,STL的list并没给断言错误,但是也运行崩溃;我这边直接用assert拿捏

接口的模拟实现:

front()和back()纠结了一下如果只剩一个头节点会怎样,用STL的试了一下会直接运行崩掉,所以我模拟时候加了assert看看是不是只剩头节点

  • front()
const T& front() {
    
    
	assert(!empty());
	return *begin();
}
  • back()
const T& back() {
    
    
	assert(!empty());
	return _head->_prev->_data;
}
  • insert()
//插入元素,在迭代器之前插
//inserting new elements before the element at the specified position
//return An iterator that points to the first of the newly inserted elements.
iterator insert(iterator pos, const T& val) {
    
    
	Node* cur = _head;
	while(cur->_next != pos._node) {
    
    
		cur = cur->_next;
	}
	Node* prev = cur;
	Node* next = cur->_next;
	Node* newnode = new Node(val);
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = next;
	next->_prev = newnode;
	return iterator(newnode);
}
  • push_front()
void push_fornt(const T& val) {
    
    
	Node* head = _head->_next;
	Node* newnode = new Node(val);
	_head->_next = newnode;
	newnode->_prev = _head;
	newnode->_next = head;
	head->_prev = newnode;
}
  • push_back()
void push_back(const T& val) {
    
    
	Node* tail = _head->_prev;
	Node* newnode = new Node(val);
	tail->_next = newnode;
	newnode->_prev = tail;
	newnode->_next = _head;
	_head->_prev = newnode;
}
  • erase()返回被删节点的下一个节点的迭代器
//return An iterator pointing to the element that followed the last element erased by the function call
iterator erase(iterator pos) {
    
    
	assert(!empty());
	Node* cur = _head;
	while(cur != pos._node) {
    
    
		cur = cur->_next;
	}
	Node* prev = cur->_prev;
	Node* next = cur->_next;
	prev->_next = next;
	next->_prev = prev;
	delete cur;
	cur = nullptr;
	return iterator(next);
}
iterator erase(iterator first, iterator last) {
    
    
	iterator it = first;
	while(it != last) {
    
    
		it = erase(it);
	}
	return it;
}
  • pop_front()
void pop_front() {
    
    
	assert(!empty());
	Node* head = _head->_next;
	_head->_next = head->_next;
	head->_next->_prev = _head;
	delete head;
	head = nullptr;
}
  • pop_back()
void pop_back() {
    
    
	assert(!empty());
	Node* tail = _head->_prev;
	tail->_prev->_next = _head;
	_head->_prev = tail->_prev;
	delete tail;
	tail = nullptr;
}
  • clear()
void clear() {
    
    
	erase(begin(), end());
}
  • swap()
void swap(list<T>& lt) {
    
    
	if(_head != lt._head) {
    
    
		std::swap(_head, lt._head);
	}
}

3.刷题

老规矩奉上若干链表的习题

leetcode:


4.vector和list的对比

vector是一个顺序表,物理上连续,逻辑上也连

  • 支持下标随机访问,O(1)
  • 尾插尾删比较快,O(1)
  • 头插头删、中间插入删除时间复杂度华为O(N)

list是一个带头双向循环链表,物理上不连续,逻辑上连续

  • 不支持下标随机访问,必须O(N)遍历
  • 任意位置的插入/删除比较快,O(1)

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

vector:学生管理系统,因为我们需要大量的随机访问,去不停地看某某个学生的信息;另一方面,一个学校的学生不会频繁增加减少,一般一年的9月份新生入学才会增加,6月毕业生毕业才会减少

list:超市货物管理:每天都得增加新货物,减少卖掉的货物,有大量的插入删除;另一方面,超市管理者不会去经常随机访问某一件货物怎么样,只要另外维护一个某一类货物的整体情况的vector就行

5.还想说的话

  1. 模拟实现stl是个无聊、耗时的过程,但是能够帮助我们很深刻地理解指针数据结构面向对象编程以及逻辑思维能力代码能力

  2. 《 STL源码剖析》------侯捷,打算读一读

  3. 本博客所有代码:githubgitee

猜你喜欢

转载自blog.csdn.net/m0_52640673/article/details/122821713
今日推荐