【C++】C++中的list

一、介绍

       官方给的 list的文档介绍

简单来说就是:

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

这个时候大家可能觉得,都是有序列表,那么和vector有什么区别和对比吗?实际上和我们学习数据结构时对链表和数组的对比很像,我来介绍一下:
 

std::list

  • std::list 是一个双向链表,支持在常数时间内对序列的任何位置进行插入和删除操作。
  • 由于其链表的性质,list 不支持快速随机访问,即不能通过索引以常数时间访问元素(例如 list[5] 是非法的)。
  • list 更适用于元素频繁插入和删除的场景,尤其是在序列的头部和尾部,或者你不需要通过索引来访问元素。
  • 迭代器失效问题较少,插入和删除操作不会导致除了被操作的元素之外的迭代器失效。
  • 在内存中不是连续存储的,因此不支持指针算术运算,并且可能导致较差的缓存性能。

std::vector

  • std::vector 是一个动态数组,可以在末尾快速地添加或移除元素(均摊常数时间复杂度),而且支持快速随机访问,即可以以常数时间访问任意位置的元素。
  • vector 的中间或开头插入或删除元素可能会导致较高的性能开销,因为这些操作需要移动插入点之后(或删除点之后)的所有元素。
  • 适用于需要经常随机访问元素,但对于插入和删除的频率较低的场景。
  • 在内存中是连续存储的,这意味着可以使用指针算术,并且有助于优化缓存使用。
  • vector 重新分配更大的内存空间以容纳更多元素时,所有的迭代器、引用和指针都可能失效。

那么list到底长什么样子呢?上图片,是不是就好理解了

二、list的使用

        作为STL(标准模板库)中的一个类,我们这篇blog的任务就是学习其的使用。

构造函数

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

上面虽然用了不少代名词,我们直接上代码例子分析自然就清楚了,分析在代码中。

(如果迭代器看不懂可以看这一篇【C++】C++中的vector-CSDN博客,里面详细介绍了)

#include <iostream>
#include <list>
using namespace std;
int main()
{
	//
	list<int> l1; // 构造空的l1
	list<int> l2(4, 100); // l2中放4个值为100的元素
	list<int> l3(l2.begin(), l2.end()); // 用l2的[begin(), end())左闭右开的区间构造l3
	list<int> l4(l3); // 用l3拷贝构造l4
	// 以数组为迭代器区间构造l5
	int array[] = { 16,2,77,29 };
	std::list<int> l5(array, array + sizeof(array) / sizeof(int));
	// 用迭代器方式打印l5中的元素
	for (std::list<int>::iterator it = l5.begin(); it != l5.end(); it++)
		std::cout << *it << " ";
	std::cout << endl;

	// C++11范围for的方式遍历
	for (auto& e : l5)
		std::cout << e << " ";

	std::cout << endl;
	return 0;
}

其实我们可以看出来,list这个类和之前的使用类的方法是基本一致的,不过他需要一个<int>来确定这个序列容器的类型,比如int,char....,就是list<int>可以当成一个整体,和vector很像。

list iterator的使用

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

接口说明

begin end

获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置 的iterator/const_iterator

rbegin + rend

获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的 reverse_iterator

 PS:

1. begin end 为正向迭代器,对迭代器执行 ++ 操作,迭代器向后移动
2. rbegin(end) rend(begin) 为反向迭代器,对迭代器执行 ++ 操作,迭代器向前移动
上代码:
#include <iostream>
#include <list>
using namespace std;

void print_list(const list<int>& l)
{
	// 注意这里调用的是list的 begin() const,返回list的const_iterator对象
	// 保护数据通过将l声明为常量引用,我们保证了在print_list函数内部无法修改列表l的内容。
	// 这意味着无法添加、删除或修改列表中的任何元素。这是一种良好的编程实践,
	// 特别是当函数的目的仅仅是读取数据而不修改数据时。
	for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it)
	{
		cout << *it << " ";
		//如果不同const 就通过
		//*it = 10; 编译不通过
	}

	cout << endl;
}
int main()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list<int> l(array, array + sizeof(array) / sizeof(array[0]));
	// 使用正向迭代器正向list中的元素
	for (list<int>::iterator it = l.begin(); it != l.end(); ++it)
		cout << *it << " ";
	cout << endl;
	// 使用反向迭代器逆向打印list中的元素
	for (list<int>::reverse_iterator it = l.rbegin(); it != l.rend(); ++it)
		cout << *it << " ";
	cout << endl;
	return 0;
}

常用的成员方法

list capacity

函数声明

接口说明

empty
检测 list 是否为空,是返回 true ,否则返回 false
size
返回 list 中有效节点的个数

列表元素访问

函数声明

接口说明

empty
检测 list 是否为空,是返回 true ,否则返回 false
size
返回 list 中有效节点的个数

 list modifiers

函数声明
接口说明
push_frontlist首元素前插入值为val的元素
pop_front删除list中第一个元素
push_backlist尾部插入值为val的元素
pop_back删除list中最后一个元素
insertlist position 位置中插入值为val的元素
erase 删除list position位置的元素
swap交换两个list中的元素
clear清空list中的有效元素
上代码看实现:
#include <iostream>
#include <list>
#include <vector>
using namespace std;
void PrintList(list<int>& l)
{
	for (auto& e : l)
		cout << e << " ";
	cout << endl;
}
//===============================================================
// push_back/pop_back/push_front/pop_front
void TestList1()
{
	cout << "TestList1()" << endl;
	int array[] = { 1, 2, 3 };
	list<int> L(array, array + sizeof(array) / sizeof(array[0]));
	// 在list的尾部插入4,头部插入0
	L.push_back(4);
	L.push_front(0);
	PrintList(L);
	// 删除list尾部节点和头部节点
	L.pop_back();
	L.pop_front();
	PrintList(L);
}
//================================================================
// insert /erase 
void TestList2()
{
	cout << "TestList2()" << endl;
	int array1[] = { 1, 2, 3 };
	list<int> L(array1, array1 + sizeof(array1) / sizeof(array1[0]));
	// 获取链表中第二个节点
	auto pos = ++L.begin();
	cout << *pos << endl;
	// 在pos前插入值为4的元素
	L.insert(pos, 4);
	PrintList(L);
	// 在pos前插入5个值为5的元素
	L.insert(pos, 5, 5);
	PrintList(L);
	// 在pos前插入[v.begin(), v.end)区间中的元素
	vector<int> v {7, 8, 9 };
	L.insert(pos, v.begin(), v.end());
	PrintList(L);
	// 删除pos位置上的元素
	L.erase(pos);
	PrintList(L);
	// 删除list中[begin, end)区间中的元素,即删除list中的所有元素
	L.erase(L.begin(), L.end());
	PrintList(L);
}
// resize/swap/clear
void TestList3()
{
	cout << "TestList3()" << endl;
	// 用数组来构造list
	int array1[] = { 1, 2, 3 };
	list<int> l1(array1, array1 + sizeof(array1) / sizeof(array1[0]));
	PrintList(l1);
	// 交换l1和l2中的元素
	list<int> l2;
	l1.swap(l2);
	PrintList(l1);
	PrintList(l2);
//使用resize将l2的大小先增加到5个元素,所有新添加的元素都将被赋值为99
	l2.resize(5, 99);
	PrintList(l2);
	// 将l2中的元素清空
	l2.clear();
	cout << l2.size() << endl;
}

int main()
{
	TestList1();
	TestList2();
	TestList3();
	return 0;
}

        好了,目前通过上面这一段精简的代码,我们把常用的成员方法基本解决了,但是list的成员方法实在太多,很多操作都是很特殊,不常见的,但是如果刚好需要又非常方便,所以就是可以在需要的时候查官方文档。

list的迭代器失效

在之前我们学习过vector的迭代器会有失效的情况,原因很简单,指针失效了,那么list会不会有这种情况呢?答案是有的,前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。所以影响相对vector来说比较小。

理解了吗?两段代码来检测一下大家

#include <iostream>
#include <list>
using namespace std;

void TestListIterator1()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list<int> l(array, array + sizeof(array) / sizeof(array[0]));
	auto it = l.begin();
	while (it != l.end())
	{
			l.erase(it);
		++it;
	}
}

void TestListIterator2()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list<int> l(array, array + sizeof(array) / sizeof(array[0]));
	auto it = l.begin();
	while (it != l.end())
	{
		l.erase(it++); // it = l.erase(it);
	}
}

int main()
{
	TestListIterator1();
	TestListIterator2();
	return 0;
}

是 TestListIterator1 出错 还是 TestListIterator2 出错?

如果你一眼就看出了,那么恭喜你,你掌握了。

其实很简单,第一个当调用erase(it)后,it被删除,使得it失效。尝试在失效的迭代器上进行操作(比如递增++it)是未定义行为。

      第二个,l.erase(it++):这里使用了“后置递增”运算符,它创建了it的一个副本,然后将副本传递给erase方法。erase删除了当前迭代器指向的元素,然后it被递增,指向下一个元素。因为it在递增前已经复制给erase,所以即使在删除当前元素后,递增操作是在一个新的、未被修改的迭代器上进行的,这保证了迭代器的有效性。或者可以这样写,等价的it = l.erase(it);erase函数返回下一个有效的迭代器,然后将其赋值给it。这样,it始终保持有效,且指向当前元素的下一个元素。

三、结语

到此为止,我们已经把list的基本使用方法学习结束了,list的成员方法十分丰富,这篇文章就是介绍了常用的,让大家基本会使用,目前你也可以用这种双向列表来实现一些复杂的算法,我在下面了可以给大家写一个。等我有时间再出一篇,模拟实现list的blog,理解他的底层实现,有缘再见,朋友!

实现的经典算法

约瑟夫环问题(Josephus Problem)。这个问题的一个版本可以描述如下:N个人围成一圈,从第一个人开始报数,每报到M时,该人被淘汰,接着从下一个人开始继续报数,直到所有人都被淘汰。任务是按顺序输出被淘汰人的编号。

#include <iostream>
#include <list>
using namespace std;

void JosephusProblem(int N, int M) {
    // 初始化人员列表,编号从1到N
    list<int> people;
    for (int i = 1; i <= N; ++i) {
        people.push_back(i);
    }

    auto it = people.begin(); // 迭代器指向第一个人
    while (!people.empty()) {
        // 模拟报数,M-1次移动迭代器(因为从当前人开始报数)
        for (int count = 1; count < M; ++count) {
            ++it;
            // 如果迭代器超过了末尾,重新从头开始
            if (it == people.end()) {
                it = people.begin();
            }
        }

        // 报到M,移除当前人,并输出编号
        cout << *it << " ";
        it = people.erase(it); // erase返回下一个元素的迭代器
        
        // 如果列表不为空,但迭代器已经到达末尾,需要重新指向开头
        if (it == people.end() && !people.empty()) {
            it = people.begin();
        }
    }
    cout << endl;
}

int main() {
    int N = 7; // 人数
    int M = 3; // 报数淘汰
    JosephusProblem(N, M);
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

浅谈TCP(2):流量控制与拥塞控制

上文浅谈TCP&#xff08;1&#xff09;&#xff1a;状态机与重传机制介绍了TCP的状态机与重传机制。本文介绍流量控制&#xff08;Flow Control&#xff0c;简称流控&#xff09;与拥塞控制&#xff08;Congestion Control&#xff09;。TCP依此保障网络的QOS&#xff08;Quali…

【Leetcode每日一题】 递归 - 求根节点到叶节点数字之和(难度⭐⭐)(50)

1. 题目解析 题目链接&#xff1a;814. 二叉树剪枝 这个问题的理解其实相当简单&#xff0c;只需看一下示例&#xff0c;基本就能明白其含义了。 2.算法原理 想象一下&#xff0c;你有一堆层层叠叠的积木&#xff0c;你想从底部开始&#xff0c;把那些标记为0的积木拿走。如…

【QT+QGIS跨平台编译】056:【PDAL+Qt跨平台编译】(pdalcpp错误处理)

点击查看专栏目录 文章目录 一、报错信息:二、原因分析三、解决思路四、原版FileUtils.cpp五、修改后的FileUtils.cpp一、报错信息: ① exists is unavaiable: introduced in macOS 10.15 ② create_directory is unavaiable: introduced in macOS 10.15 ③ create_director…

BCLinux-for-Euler配置本地yum源

稍微吐槽一句…… 在这片土地上&#xff0c;国产化软件的大潮正在滚滚而来&#xff0c;虽然都不是真正意义上的国产化&#xff0c;但是至少壳是国产的~~~ 之前使用的Centos7的系统&#xff0c;现在都要求统一换成BCLinux-for-Euler。说实话换了之后不太适应&#xff0c;好多用习…

练习实践-TLS02-会话恢复的两种形式-Session ID/SessionTicket

参考来源&#xff1a; 书籍&#xff1a;深入浅出https-从原理到实战&#xff08;作者&#xff1a;虞卫东&#xff09; 抓包分析文件可下载&#xff0c;来自github上的作者上传资源 会话恢复机制的背景 当客户端和服务器端握手成功&#xff0c;建立了一个完整的 TLS 连接&…

刷题之Leetcode35题(超级详细)

35.搜索插入位置 力扣题目链接(opens new window)https://leetcode.cn/problems/search-insert-position/ 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 你可…

ChatGPT 与 OpenAI 的现代生成式 AI(下)

原文&#xff1a;Modern Generative AI with ChatGPT and OpenAI Models 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 七、通过 ChatGPT 掌握营销技巧 在本章中&#xff0c;我们将重点介绍营销人员如何利用 ChatGPT&#xff0c;在这一领域中查看 ChatGPT 的主要用例…

jvisualvm 使用教程

之前看过 jvisualvm&#xff0c;但是那个时候对 JVM 并不是很熟悉&#xff0c;后面看了下八股文&#xff0c;看了下 JVM 的相关知识之后&#xff0c;发现多了解点 JVM 的东西&#xff0c;对我们 CRUD 其实是有指导意义的&#xff0c;就比如我们通常会 new 一堆的没有用到的对象…

读所罗门的密码笔记10_寒武纪国家与城堡国家

1. DARPA 1.1. DARPA仍然是世界领先的尖端高科技研究推动者之一&#xff0c;资助研发人员开展各个领域的前沿研究&#xff0c;从自动驾驶汽车到植入式神经芯片&#xff0c;从复杂的系统分析&#xff08;比如分析气候变化&#xff09;到网络安全&#xff0c;无一…

31. 下一个排列 —— LeetCode (python) [PS: LeetCode 运行环境疑似出错]

# encoding utf-8 # 开发者&#xff1a;xxx # 开发时间&#xff1a; 20:26 # "Stay hungry&#xff0c;stay foolish."class Solution(object):def nextPermutation(self, nums):import itertoolsl len(nums)a tuple(nums)nums.sort()permutations_lst list(ite…

DDD 的四层领域模型是怎样的?包含哪些基础概念?

DDD的四层领域模型如下所示&#xff1a; 展现层&#xff1a;这一层负责向用户显示信息和解释用户命令&#xff0c;完成前端界面逻辑。并将用户请求传递给应用层。应用层&#xff1a;这一层是很薄的一层&#xff0c;负责协调领域层中的领域对象&#xff0c;组成具体应用场景。应…

JAVA JVM内存模型和GC分配和回收

Java 的JVM简介 JVM是&#xff08;Java Virtual Machine&#xff09;Java虚拟机的缩写。 JVM是一个虚构出来的计算机&#xff0c;是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 ​ 在Java程序运行时&#xff0c;所有的.class类需要加载到JVM中才能执行代码逻辑。不…

Python环境下基于离散小波变换的信号降噪方法

Mallat创造了小波分析中的经典理论之一&#xff0c;即多分辨率分析的概念。后来&#xff0c;在Mallat与Meyer的共同努力之下&#xff0c;他们又在这一理论的基础上发明了离散小波变换的快速算法&#xff0c;这就是Mallat塔式算法&#xff0c;这种算法可以大量减少计算时间。在之…

解锁未来:大模型GPT的应用架构与创新实践

在人工智能的黄金时代&#xff0c;大模型如GPT&#xff08;Generative Pre-trained Transformer&#xff09;已成为技术创新和应用发展的前沿。它不仅重新定义了人机交互的方式&#xff0c;还在多个领域内展现出了巨大的应用潜力。本文将深入探讨大模型GPT的应用架构&#xff0…

深入解析:链游、DApp、公链、NFT与交易所开发的全景图

随着数字货币和区块链技术的迅速发展&#xff0c;链游开发、DApp开发、公链开发、NFT开发以及交易所开发等领域吸引了越来越多的关注。本文将以3000字的篇幅&#xff0c;对这些领域进行详细解析&#xff0c;探讨它们的意义、应用场景以及未来发展趋势。 链游开发&#xff08;Bl…

基于keepalived+gtid+双vip半同步主从复制的MySQL高性能集群

项目名称&#xff1a;基于keepalivedgtid双vip半同步主从复制的MySQL高性能集群 目录 项目名称&#xff1a;基于keepalivedgtid双vip半同步主从复制的MySQL高性能集群 项目规划图 1.配置4台MySQL服务器&#xff08;1台master&#xff0c;2台slave&#xff0c;1台backup&a…

光伏无人机:绿色能源与航空技术的融合创新

在可再生能源和无人机技术快速发展的背景下&#xff0c;光伏无人机作为一种新兴的绿色航空器&#xff0c;正逐渐展现出其独特的优势和广阔的应用前景。本文将深入探讨光伏无人机的原理、优势以及其在多个领域的应用&#xff0c;展望其未来的发展趋势。 一、光伏无人机的原理 光…

基于微信小程序的外卖管理系统的设计与实现(论文+源码)_kaic

摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对高校教师成果信息管理混乱&#xff0c;出错率高&#xff0c;信息安全…

【VSCode】修改插件地址

不想放在原始C盘下面C:\Users\{用户}\.vscode\extensions为了后续存储空间考虑&#xff0c;想通过添加环境变量创建名为VSCODE_EXTENSIONS的环境变量&#xff0c;内容指向vs Code扩展所在目录即可 直接配置环境变量&#xff0c;不要在有空格的文件夹下面 变量名称&#xff1a;…

『VUE』11. 操作数组的方法(详细图文注释)

目录 vue中操作数组的方法会修改原数组的 会进行渲染更新不修改原数组的 不会进行渲染更新 push自动渲染concat 赋值渲染总结 欢迎关注 『VUE』 专栏&#xff0c;持续更新中 欢迎关注 『VUE』 专栏&#xff0c;持续更新中 vue中操作数组的方法 vue中数组数据呈现在网页,只检测…