【C++STL详解】带头双向循环结构 + 双向迭代器,核心接口 + 排序效率 + 避坑指南 - 教程

news/2025/9/26 14:31:38/文章来源:https://www.cnblogs.com/lxjshuju/p/19113440

封面

个人主页:爱和冰阔乐
专栏传送门:《数据结构与算法》 、C++
学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然

在这里插入图片描述


博主简介
在这里插入图片描述

文章目录

  • 前言
  • 一、list的结构
  • 二、list核心接口
    • 2.1 构造函数
    • 2.2 迭代器/范围for遍历list
    • 2.3 emplace_back与push_back的比较
    • 2.4 insert插入与erase删除操作
    • 2.5sort排序
    • 2.6 合并两个有序链表merge接口
    • 2.7unique去重
    • 2.8 splice 粘接
    • 2.9 链表排序效率
  • 三、资源分享
  • 四、总结


前言

STL的学习都是类似的,在学完vector后,不可避免的要学习list(链表),在这里我们将实现带头双向循环链表,与C语言中已经实现的链表有异曲同工之妙,如若有兴趣也可以看看C语言实现链表 手动实现单链表与双链表的接口及OJ挑战

一、list的结构

list是带头双向循环链表,下面我们将分析下其中的含义:

双向: 每个节点都有prev(前驱)和next(后继)指针,可正向 / 逆向遍历;
循环: 尾节点的next指向头结点,头结点的prev指向尾节点,形成闭环;
头结点/哨兵位: 不存储实际数据,仅用于统一接口(避免插入 / 删除首节点时的特殊处理)

在这里插入图片描述

二、list核心接口

在学习每个容器前我们都需要了解下其的文档介绍 list文档介绍

2.1 构造函数

在这里插入图片描述

list和前面学习的lvector一样,分为四种:
1.全缺省构造(默认构造)
2.n个val构造
3.迭代器区间构造
4.拷贝构造

// 1. 空list
list<
int> l1;
// 2. 5个元素,均为3
list<
int>
l2(5, 3);
// [3,3,3,3,3]
// 3. 用数组区间初始化
int arr[] = {
1,2,3,4,5
};
list<
int>
l3(arr, arr+5);
// [1,2,3,4,5]
// 4. 拷贝构造
list<
int>
l4(l3);
// [1,2,3,4,5]

同样,析构和赋值与之前STL容器的实现几乎一样,不再进行实现

2.2 迭代器/范围for遍历list

在链表中不再支持下标+[ ]遍历,因为如果想要获取第n个数据,必须要重新遍历,时间复杂度过高(不再像数组一样是连续的物理空间,获取数据的复杂度为O(1),list的底层节点实现地址并不连续)

此处,大家可暂时将迭代器理解成一个指针,该指针指向list中的某个节点

在这里插入图片描述

在这里插入图片描述

注意:

  1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
  2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动(不是- -)

遍历链表

void test_list1()
{
list<
int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
//下标+[ ]便不可以了,我们使用迭代器
list<
int>
::iterator it=lt.begin();
while(it!=lt.end())
{
cout<<
*it<<
" ";
++it;
}
cout<<endl;
for(auto e:lt)
{
cout<<e<<
" ";
}
cout<<endl;
}

这里需要注意的是如果要删除链表某个位置的顺序不可以使用如下写法

//错误写法
it=ltbegin();
lt.erase(it+3);

在这里插入图片描述

在这段代码我们发现代码报错,我们回想下前面学习vector为什么可以,原来vector实现迭代器是原生指针,而在list这里不再是原生指针,那么为什么list不可以使用原生指针的原因还是其底层不再是连续的地址

总结:
迭代器按照功能划分 : iterator        reverse_iterator        const_iterator        const_reverse_iterator

性质划分: 1.单向:forwad_list(单链表)/unordered-map… 只支持++        2.双向:list/map/set 支持++/- - (不支持+/-)        3.随机:vector/string/deque… 支持++/–/+/-

list:迭代器
在这里插入图片描述
vector:随机迭代器
在这里插入图片描述

unordered_map:单向迭代器

在这里插入图片描述

决定迭代器的性质是底层的结构决定的

性质决定了可以使用哪些算法,下面我们根据sort/reverse等算法来深入了解下:
在这里插入图片描述

sort是随机迭代器实现的,可以支持++/- -/+/-,并且其是由模板实现的,那么是不是所有的容器均可以排序,那当然不可以,sort的时候,迭代器只能使用随机迭代器,那么list便不行,因为其不支持 + / -

list<
int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
sort(lt.begin(),lt.end());

在这里插入图片描述

因此我们想要对list进行排序必须使用list自带的排序算法

list<
int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.sort();
for (auto t : lt)
{
cout << t <<
" " ;
}
cout << endl;

reverse传双向迭代器,因此双向迭代器可以支持list,但不支持单向迭代器,因为在其实现时使用了- -,那么随机迭代器可=可不可以使用这里的逆置,那当然可以了,因为逆置实现的是++/- -,而随机迭代器本身也支持++/- -
在这里插入图片描述

find的迭代器:所有提供输入迭代器的容器,在后面继承我们会学习

在这里插入图片描述

2.3 emplace_back与push_back的比较

从日常的角度,用push_back和emplace_back是一样的,在日常插入一个数据时,不管使用有名对象还是匿名对象,push_back支持的,emplace_back也支持,如下

struct A
{
public:
A(int a1 = 1, int a2 = 1)
:_a1(a1)
,_a2(a2)
{
cout <<
"A(int a1=1,int a2=1)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
,_a2(aa._a2)
{
cout <<
"A(const A& aa)" << endl;
}
int _a1;
int _a2;
};
list<A> lt;A aa1(1, 1);//有名对象lt.push_back(aa1);//匿名对象lt.push_back(A(2, 2));lt.emplace_back(aa1);lt.push_back(aa1);lt.emplace_back(A(2.2));

>emplace_back还支持如下写法

lt.emplace_back(3, 3);

在这里插入图片描述

但是push_back不可以

在这里插入图片描述

因为push_back只支持一个参数——A类型的对象,但是emplace_back是可变类型的模板,因此支持直接传构造A的参数,由于我们并没有了解很多C++的知识,后续会对emplace_back再进行介绍,

我们只需要知道push_back 需要先有一个 A 类型的对象(无论是有名对象如 aa1,还是匿名对象如 A(2, 2)),然后将这个对象复制或移动到容器中(会触发拷贝构造或移动构造),即先构造再拷贝构造,emplace_back 则直接在容器内存中调用 A 的构造函数创建对象,没有拷贝构造,效率更高即可

2.4 insert插入与erase删除操作

在这里插入图片描述

这里的插入也和vector类似

1.在pos位置之前插入val
2.pos位置之前插入n个val
3.pos位置之前插入一段迭代器区间

但是我们知道list的地址并不是连续的,那么想要在指定位置之前插入数据,不能像vector一样使用(begin/end + n),list的迭代器是双向的,不支持随机的 + / -,因此我们只能定义新的变量代表在第i个位置前插入数据,使用while循环,i递减,让迭代器向目标位置递增

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 k = 3;
//while循环遍历找到要插入数据的位置
while (k--)
{
//list 的迭代器(双向迭代器)已经封装了链表节点的指针跳转逻辑,
// ++it 并不是直接对物理地址进行加减,而是通过链表节点内部存储的next指针跳转到下一个位置的
++it;
}
lt.insert(it, 30);
for (auto e : lt)
{
cout << e <<
" ";
}
cout << endl;

在这里插入图片描述

erase删除数据
在这里插入图片描述

在删除数据前,我们现需要输入删除的数据在list中是否存在,那么就需要使用到算法中find函数对list进行查找,这里我们想把1 2 3 30 4 5 6中的3删掉

int x=0;
cin>>x;
it=find(lt.begin(),lt.end());
if(it!=lt.end())
{
lt.erase(it);
}

结果如下:

在这里插入图片描述

2.5sort排序

算法库中的sort是随机迭代器,而list对应的是双向迭代器,因此list自己必须要实现一个sort接口(无论是算法库中的还是list实现的均是默认排升序

list<
int> lt;
lt.push_back(1);
lt.push_back(3);
lt.push_back(2);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
//这里sort排序默认排的是升序
lt.sort();

在这里插入图片描述

那么如果我们想给list排降序,就需要用到仿函数,在后面栈和队列中会有所介绍

在排升序的时候,我们使用的数学符号为 “ <“ (小于),小于对应的英文为less
在排降序时,使用的数学符号为“ > ”(大于),对应的英文是greater

less<
int> ls;
//类模板
//有名对象
greater<
int> gt;
//类模板
//匿名对象
lt.sort(greater<
int>());
lt.sort(gt);

排降序
在这里插入图片描述

2.6 合并两个有序链表merge接口

在merge接口文档介绍中我们看到合并两个链表的前提是均是有序的
在这里插入图片描述
下面是官方给的案例:
在这里插入图片描述

从两个链表的头部开始,逐个比较元素大小,将较小的元素(按排序规则)依次插入到结果链表中
当其中一个链表的元素全部处理完后,将另一个链表中剩余的元素直接拼接过来

int main() {
list<
int> a = {
1, 3, 5
};
list<
int> b = {
2, 4, 6
};
// a和b都是升序
a.merge(b);
// 合并后a变为{1,2,3,4,5,6},b变为空
for (int x : a)
cout << x <<
" ";
// 输出:1 2 3 4 5 6
return 0;
}

2.7unique去重

在这里插入图片描述

该接口的作用是删除链表中重复的元素,只会保留一个,去重要求数据必须是有序的,否则会出粗,下面我们通过代码来演示下

list<
int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(2);
lt.push_back(2);
lt.push_back(2);
lt.push_back(30);
lt.push_back(2);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
lt.push_back(7);
for (auto e : lt)
{
cout << e <<
" ";
}
cout << endl;
//unique去重要求数据必须是有序的,否则会出粗,因此可以sort下
//lt.sort();
lt.unique();
for (auto e : lt)
{
cout << e <<
" ";
}
cout << endl;

结果演示:
在这里插入图片描述

果然,我们发现使用unique去重后,还剩下2个2,因为list实现该接口时,默认list是有序的,即相同的值是连在一起的,因此在去重前,我们需要sort下,这里我们扩展想下,如果链表是有序的,那么去重我们自己实现也很简单,在前面C语言的数据结构章节我们介绍了双指针法便可以轻松解决

2.8 splice 粘接

在这里插入图片描述splice接口的含义是:剪切后再粘贴,不要与复制粘贴搞错乱。

下面我们举个简单易懂的生活小例子:在生活中如果我们创业,那么肯定希望钱能生钱,这对应的便是复制粘贴,不动我的本金。那么如果在创业时,因为轻信他人被诈骗,导致本金转移到别人手里,那么这就是splice的含义,本质就是转移

下面我们通过splice给的文档代码来看看具体的步骤是如何的:

在mylist1中插入了1 2 3 4四个数据,在mylist2中插入了10 20 30,再把mylist2中的数据转移到mylist1中2数据之前
在这里插入图片描述

注意:不是把10 20 30拷贝到mylist1上面,而是把mylist2中的数据剪切走(mylist2就空了),再粘贴到mylist1中

在下面我看看一种常见的场景,我们希望把链表的顺序转移下,在下面链表中我们假设把6转移到1之前

在没学splice之前,我们可以把6删除,再在1之前插入6,由于释放了一个节点又增加了一个节点,时间复杂度便会很高

我们希望直接把6这个节点直接转移到1前面,那么首先我们需要find下找到目标节点,然后调用splice(lt.begin(),lt,it)即可

splice(lt.begin(),lt,it)是指将lt的it值转移到begin()之前

那么我们想要将目标节点及其之后的节点均移动到begin()之前只需给一段迭代区间即可
lt.splice(lt.begin(), lt, it, lt.end())

int x = 0;
cin >> x;
auto it = lt.begin();
//find查找+删除数据
it = find(lt.begin(), lt.end(), x);
if (it != lt.end())
{
//只转移it这个数据
//lt.splice(lt.begin(), lt,it); 
//如果转移从该位置到最后的一段数据
lt.splice(lt.begin(), lt, it, lt.end());
}

将目标节点转移至begin()位置
在这里插入图片描述
将目标元素及其之后的数据移动到begin()之前
在这里插入图片描述

2.9 链表排序效率

如果链表的数据量小可以使用list自带的sort进行1排序,但是一旦数据量很大则排序效率低下,下面我们通过将链表放在顺序表中排序和使用list自带的排序在debug版本下跑下

srand(time(0));
const int N = 1000000;
list<
int> lt1;
vector<
int> v;
for (int i = 0; i < N;
++i)
{
auto e = rand() + i;
lt1.push_back(e);
v.push_back(e);
}
int begin1 = clock();
// 算法库中的sort排序
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
//list自带的sort排序
lt1.sort();
int end2 = clock();
printf("vector sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);

我们发现即使在优化没有全开的debug版本下vector排序更高
在这里插入图片描述

那么在release版本下显而易见,vector排序更快(注意在测性能时不用debug,不具有参考价值)

debug下不具有参考价值的原因可以通过生活中的小例子对比:将一岁的儿童和苏炳添放在一起进行百米冲刺比赛,在日常情况下,苏炳添会为了照顾小朋友会跑的快那么几米,可真正在比赛场上苏炳添全力冲刺肯定远比小朋友跑的远得多,因此我们只会以优化全开的release进行判断

由于vector底层的sort使用了快排,快排需要用到递归,递归在debug版本下会打很多的调试信息,建立栈帧等导致时间没有相差多少

在这里插入图片描述

如果需求是将链表进行排序,那么将list的迭代区间传给vector还需要再拷贝回来,那么是否还是比直接调用list的sort排序快?
下面我们通过创建两个list对象进行比较

srand(time(0));
const int N = 1000000;
list<
int> lt1;
list<
int> lt2;
for (int i = 0; i < N;
++i)
{
auto e = rand() + i;
lt1.push_back(e);
lt2.push_back(e);
}
int begin1 = clock();
// 拷贝vector
vector<
int>
v(lt2.begin(), lt2.end());
// 排序
sort(v.begin(), v.end());
// 拷贝回lt,这里只能使用assign,因为不同容器之间无法赋值
lt2.assign(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
lt1.sort();
int end2 = clock();
printf("list copy vector sort copy list sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);

显而易见,使用vector排序即使多了两次拷贝,依旧比直接排序快,即苏炳添让1岁小孩先跑三米,也最后超越小孩

在这里插入图片描述

三、资源分享

1.list官方文 :https://cplusplus.com/reference/list/list/?kw=list
2.list重要接口实现案例:https://gitee.com/zero-point-civic/c-initial-stage/tree/master/list/list

四、总结

在C语言实现单向链表与双向链表中我们已经熟悉了链表的基本底层构成是如何实现的,在本文我们需要注意C++下的list的不同接口的实现及其避坑,在下一章中我们会简单模拟实现下链表的底层,让大家对list更加熟悉,敬请期待下节分解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/918389.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学做视频的网站有哪些window7用jsp做的网站要什么工具

距离vue3.3发布已经过了一年多(2023.5.11),vue3.3提高开发体验的新特性你用了吗&#xff1f; 组件内部导入复杂类型 3.3之前想在组件内部导入复杂类型做props类型是不支持的。 <script setup lang"ts">import type { People } from /types;withDefaults(define…

Node.js后端学习笔记:Express+MySQL - 指南

Node.js后端学习笔记:Express+MySQL - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", &quo…

TorchV知识库安全解决方案:基于智能环境感知的动态权限控制

TorchV知识库安全解决方案:基于智能环境感知的动态权限控制在数据即资产的时代,企业知识库的安全不再仅限于“用户名和密码”。TorchV带来的是一种全新的安全范式——让环境成为新的安全边界。————TorchV舒一笑不…

详细介绍:Java HTTP协议(二)--- HTTPS,Tomcat

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

VBA ETH功能应用 | “0”代码构建SOME/IP节点

在智能汽车飞速发展的今天,车载以太网通信已成为功能服务化的核心驱动力。工程师在控制器开发、通信集成和测试验证中,常常面临SOMEIP协议交互的复杂挑战:SD过程繁琐、SessionID等字段具有变化逻辑、纯手动构造二层…

ISUP协议视频平台EasyCVR在智慧灯杆综合管理中的应用

ISUP协议视频平台EasyCVR在智慧灯杆综合管理中的应用智慧城市的迅猛发展带动了智慧灯杆综合管理平台的兴起,平台作为城市基础设施智能化的关键组成部分,日益彰显其在城市管理和公共服务领域的显著优势。通过整合多样…

视觉智能赋能产业数智化升级:JBoltAI多模态技术落地实践

视觉智能赋能产业数智化升级:JBoltAI多模态技术落地实践一、多模态技术:企业数智化转型的新引擎当前,全球产业正面临着一个共同挑战:如何将海量的非结构化数据转化为可操作的商业智能。传统企业系统中,大量信息以…

神秘考试题

题意 有一个长度为 \(n\) 的序列 \(a\),你需要求出: \[\sum_{i = 1}^n \sum_{j = 1}^n (a_i \text{and} a_j)(a_i \text{or} a_j)(a_i \text{xor} a_j) \]solution 首先对于每个结果拆位,也就是对于原式中的三项,我…

Solon v3.4.6, v3.5.4, v3.6.0-M1 发布。正式开始 LTS 计划

Solon 是新一代 Java 企业级应用开发框架,采用 Apache 2.0 开源协议。相比 Spring,Solon 在计算性价比、开发效率、生产部署等方面优势明显,并发性能提升 700%,内存节省 50%,启动速度快 10 倍,同时兼容 Java 8-2…

串口通信、阻塞与非阻塞、qt

【问题】当自己码完收发代码之后,确收、发不成功串口数据。 【解题思路】在确保代码无误的情况下,很可能是阻塞与非阻塞通信的区别。所以,需要分别尝试实现阻塞与非阻塞的方式进行测试。 【深层次原因理解】拿qt工程…

网站建设运营案例怎样做app网站建设

promise请求数据用法 Promise简介 Promise 是异步编程的一种解决方案&#xff0c;比传统的解决方案–回调函数和事件&#xff0d;&#xff0d;更合理和更强大。ES6将其写进了语言标准&#xff0c;统一了语法&#xff0c;里面保存着某个未来才回结束的事件(通常是一个异步操作&a…

织梦网站底端的怎么删除满足seo需求的网站

提示&#xff1a;今日是2024年的6月30日&#xff0c;未来的你看到这篇文章&#xff0c;希望你依旧快乐 文章目录 前言 首先在这里前缀部分我就不做要求了,比如说登录信息什么的 数据库表格 这里实现点赞功能&#xff0c;主要是围绕论坛项目完成的 user_info代表用户信息表 for…

破解 Java 系统 AI 化难题:JBoltAI 框架自带 RAG、Function Calling 核心功能

破解 Java 系统 AI 化难题:JBoltAI 框架自带 RAG、Function Calling 核心功能在 AI 技术进入企业服务的当下,Java 作为承载企业核心业务的 “基石语言”,却面临着一场尴尬的 “适配断层”:多数 Java 系统仍停留在传…

CF1706E Qpwoeirut and Vertices

一个较为简单的题目,做起来比较舒服。 题目 有 \(N\) 个点 \(M\) 条边。 有 \(Q\) 个询问,每个询问有 \(L,R\)。 询问 \(L\le a \le b \le R\) 最少需要前几条边才能联通。 都是 \(1e5\) 级别。 做法 我们把第 \(i\)…

聚焦 Java AI 开发:JBoltAI 框架支持多模型适配,打造智能应用

聚焦 Java AI 开发:JBoltAI 框架支持多模型适配,打造智能应用在 AI 技术加速融入企业业务的当下,Java 技术团队却面临着一道 “两难困境”:想接入大模型提升系统智能度,却要为不同厂商的接口重复封装代码;想改造…

企业级 AI 应用开发首选!JBoltAI 框架适配 Java 技术栈,稳定可靠

企业级 AI 应用开发首选!JBoltAI 框架适配 Java 技术栈,稳定可靠一、Java 企业 AI 转型的 “现实困境” 与 JBoltAI 的破局价值Java 技术团队却面临着一道 “转型鸿沟”:想接入大模型能力,却因工程师自主封装接口的…

电器网站建设流程网站建设公司好发信息网

摘要(From AI): 这篇笔记全面介绍了 FreeRTOS 软件定时器的核心概念和使用方法&#xff0c;包括定时器的创建、管理、常用 API 和辅助函数&#xff0c;并通过示例代码演示了如何启动、重置和更改定时器的周期。它强调了软件定时器的灵活性、平台无关性以及与硬件定时器的对比 …

AIGS架构革命:JBoltAI如何重塑Java企业的AI服务生态

AIGS架构革命:JBoltAI如何重塑Java企业的AI服务生态AI数智化转型的十字路口2025年,人工智能产业迎来三维共振的关键节点。根据中国信通院发布的2025人工智能十大关键词,“基础超级模型”、“自主性更强的智能体” 和…

RAG技术赋能企业数智化转型:JBoltAI如何破解AI落地“最后一公里”难题

RAG技术赋能企业数智化转型:JBoltAI如何破解AI落地“最后一公里”难题01. 人工智能时代的产业变革与挑战2025年,人工智能不再是遥远的概念,而是推动产业变革的核心动力。然而,对于大量传统企业而言,AI转型之路并不…