list容器详解——双向链表的封装与使用

本文代码已同步Github

一、为什么需要list

在上一篇文章中,我们学习了vector以及底层原理

在 C++ STL 中,vector 是我们最常使用的容器之一;

它底层采用动态数组实现,具有连续的内存空间,因此支持快速随机访问。

同样vector可以直接使用下标来访问元素,这也是vector的最大优势

但是,vector不是万能的,当我们需要频繁在容器中间进行插入和删除操作时

由于vector中的元素是连续存储,因此对于vector便会挪动 数据,导致效率会明显下降

此时,我们便需要一种新的数据结构:

  • 不要求元素连续存储
  • 插入和删除元素时不需要大量移动数据

这就是链表(List)

std::list底层采用双向链表实现,每个元素都是一个单独的节点

插入 或者删除仅需修改指针关系即可;


为什么会提供list?

不同的数据结构适用于不同的场景:

  • 如果需要大量查询、随机访问:
    • 选择 vector
  • 如果需要频繁插入、删除:
    • 选择 list

STL 并没有设计一个"万能容器",而是提供了多种容器,让开发者根据实际需求进行选择。

vector 追求的是访问效率,而 list 追求的是修改效率。

因此,list 的存在并不是为了替代 vector,而是为了弥补 vector 在频繁插入和删除场景下的不足

二、list和vector区别

vectorlist都是STL中非常常用的容器,但由于底层结构不同,导致使用不同的场景;

简单来说:

vector适合随机访问,list适合插入与删除

1、底层结构不同

vector底层是一段连续的内存空间

因此可以通过下标随机进行访问;

时间复杂度为O(1)


list底层是一个一个独立的节点;

节点之间通过指针进行连接;

不能使用下标进行访问,一般使用迭代器进行遍历

想要访问第n个节点,就需要循环n次,一个一个节点向下找

时间复杂度为O(N)

2、插入和删除效率不同

vector中进行插入和删除时都需要挪动数据;

时间复杂度为O(1)

list中进行插入和删除时仅需修改指针指向即可,无需移动其他元素;

如果已经找到要插入或删除的位置,仅需**O(1)**时间即可

注意⚠️:如果没有找到位置,就需要先遍历链表,找到对应位置,这是**O(N)**级别

3、迭代器不同

我们仔细来看vector中的迭代器

发现:vector中是随机迭代器

再来看list

发现:list中是双向迭代器

只能**++/--**

4、总结对比

特点 vector list
底层结构 动态数组 双向链表
内存结构 连续 不连续
随机访问 O(1) O(n)
头部插入 O(n) O(1)
中间插入 O(n) O(1)(已有迭代器)
删除元素 O(n) O(1)(已有迭代器)
空间利用
缓存友好
迭代器类型 随机访问迭代器 双向迭代器
支持下标 ×

三、常用接口

vector一样,我们先给出list接口文档:list的使用参考文档

1、默认成员函数

① 首先来看构造函数

整理出常用接口:

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

我们依次来使用

在使用之前先实现一个打印容器的函数模板

cpp 复制代码
template<class T>
void print_container(const T& val)
{
	for (auto& e : val)
	{
		std::cout << e << " ";
	}
	std::cout << std::endl;
}

接着来测试试试


析构函数

析构函数就是在list对象生命周期结束时清理资源,自动调用;


赋值重载运算符

注意⚠️:两个已经存在的对象赋值才会调用赋值重载运算符;否则便会调用拷贝构造

来测试一下:

2、迭代器

整理出常用接口:

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

我们依次来使用

3、容量

整理出常用接口:

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

来使用一下

4、元素访问

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

来测试一下

5、修改操作

整理出常用接口:

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

我们来使用这些接口

四、迭代器特点

  1. 类型限制双向迭代器 ,只能 ++-- 移动,不支持 +n[] 或比较大小。
  2. 失效规则极其稳定。插入和删除元素时,仅被删元素的迭代器失效,其他所有迭代器保持有效。
  3. 底层本质 :封装了链表节点的指针,移动迭代器就是移动 node->next/prev

接着通过一张表格来和vector的迭代器来比较一下

对比维度 std::list 迭代器 std::vector 迭代器
类型(类别) 双向迭代器 随机访问迭代器
支持的运算 ++--==!= 支持 ++--+n-n[]<><=>=
底层本质 封装链表节点指针(Node* 封装连续数组元素指针(T*
插入元素时失效 永不失效 若扩容则全部失效;未扩容则插入位置之后的失效
删除元素时失效 仅被删除元素失效,其他迭代器保留 被删元素及其之后的所有迭代器失效
内存布局 非连续(节点分散于堆中) 连续内存块
随机访问耗时 O(n),必须逐节点移动 O(1),直接指针偏移计算
对 end() 操作 end() 是尾后哨兵,--end() 合法(前提非空) end() 是尾后指针,--end() 合法(前提非空)

五、特有接口

下面来看看list的特有接口

我们整理成表格

函数声明 接口说明
splice 将另一个list中的元素转移到当前list的指定位置
remove 移除list中所有与给定值相等的元素
remove_if 移除list中所有满足谓词条件的元素
unique 移除list中连续重复的元素,只保留一个
merge 合并两个已排序的list,合并后仍保持有序
sort 对list中的元素进行排序(默认升序)
reverse 反转list中元素的顺序

我们同样依次来使用

六、迭代器失效

最后来看迭代器失效问题:

首先来思考一下,对于insert是否有迭代器失效问题?

vector中,当在pos位置之前插入一个元素之后,由于相对位置的改变 ,我们认为迭代器失效了

而在list中,插入一个节点之后,仅仅改变了指针的链接,相对位置并未发生改变,因此,listinsert之后迭代器不失效

接着来看erase:

vector中,删除pos位置的元素之后,pos位置的位置失效;

list中,删除pos节点之后,指向当前节点的迭代器同样失效,但其他位置的迭代器并无影响;

如果觉得有帮助,可以关注Github项目持续更新

相关推荐
Nil2082 小时前
C++ emplace_back 与 push_back 详解
开发语言·c++
漂流瓶jz2 小时前
UVA-12174 Shuffle的播放记录 题解答案代码 算法竞赛入门经典第二版
数据结构·c++·算法·图论·滑动窗口·算法竞赛入门经典·uva
Darkwanderor3 小时前
线程概念和Linux线程
linux·c语言·c++
lingran__3 小时前
C++ 高阶数据结构:红黑树万字详解|完整原理推导 + 插入实现 + 完整性校验【STL 底层】
数据结构·c++·面试·红黑树·二叉搜索树·平衡二叉树·stl底层
东东最爱敲键盘3 小时前
day9.C++多态之虚函数
开发语言·c++
Darkwanderor3 小时前
Linux线程控制
linux·c语言·c++
moonsims4 小时前
低空量子无人机
前端·数据结构
love_muming4 小时前
二叉树操作全解析:从递归到层序遍历
java·数据结构·算法·二叉树
Darkwanderor4 小时前
多线程的互斥问题和互斥锁解决互斥问题
linux·c语言·c++