【C++】容器篇(二)——List的基本概述以及模拟实现

news2024/11/18 3:42:47

前言:

在上期,我们学习了STL库中的第一个容器--vector ,今天我将给大家介绍的是 库中的另外一个容器--List。其实,有了之前学习 vector 的知识,对于List 的学习成本就很低了。


目录

(一)基本介绍

1、基本概念

2、list 与 forward_list 的比较

3、特点

(二)list的使用

1、list的构造

2、 list iterator的使用

3、list capacity

4、list element access

5、list modifiers

6、list的迭代器失效

1、失效时机

2、list与vector迭代器失效对比:

1️⃣vector

2️⃣list

(三)list的模拟实现

1、代码展示

2、注意事项

1️⃣【】不能实现访问操作

2️⃣sort()

(四)list与vector的对比

(五)总结 


(一)基本介绍

1、基本概念

  1.  list 是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
  2.  list 的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向 其前一个元素和后一个元素。

2、list 与 forward_list 的比较

list与forward_list非常相似,最主要的区别如下:

  • 最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。

3、特点

与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。

与其他序列式容器相比,list 和 forward_list 最大的缺陷是不支持任意位置的随机访问

  1. 比如:要访问list 的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;
  2. list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这 可能是一个重要的因素)


(二)list的使用

list的文档介绍:文档介绍

 list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展 的能力。以下为list中一些常见的重要接口。


1、list的构造

2、 list iterator的使用

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

 

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

 

3、list capacity

4、list element access

 

 

5、list modifiers

  • list中还有一些操作,需要用到时大家可参阅list的文档说明。 

6、list的迭代器失效

前面已经跟大家说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了

1、失效时机

因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

⚔️ 注意 ⚔️

迭代器有两种实现方式,具体应根据容器底层数据结构实现:

  • 1️⃣原生指针:比如:vector的迭代器就是原生指针;
  • 2️⃣将原生指针进行封装,因迭代器使用形式与指针完全相同,迭代器就是自定义类型对原生指针的封装,模拟指针的行为。

2、list与vector迭代器失效对比:

在C++中,当使用STL容器中的元素并且对容器进行修改时,可能会导致迭代器失效。在这种情况下,如果试图继续使用已经失效的迭代器,则程序可能会产生未定义的行为。


1️⃣vector

在使用vector容器时,添加或删除元素可能会导致迭代器失效。

  1. 这是因为vector容器内部使用动态数组实现,当容器中的元素数量超过了其内存分配的大小时,就需要重新分配一块更大的内存,并将原有元素拷贝到新的内存中;
  2. 此时,原有的迭代器就无法正确指向其对应的元素,因为元素的位置已经改变了;
  3. 同样的情况也发生在删除元素时,因为删除元素后,后面的元素会向前移动,导致原有的迭代器失效。

2️⃣list

插入元素不会导致迭代器失效, 删除元素时,只会导致当前迭代 器失效,其他迭代器不受影响。

void Test()
{
     int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
     list<int> ll(array, array+sizeof(array)/sizeof(array[0]));
     auto it = ll.begin();
     while (it != ll.end())
     {
         // erase()函数执行后,it所指向的节点已被删除,因此it无效
         //在下一次使用it时,必须先给其赋值
         ll.erase(it); 
         ++it;
     }
}

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

(三)list的模拟实现

要模拟实现list,必须要熟悉list的底层结构以及其接口的含义,通过上面的学习以及之前对vector和string的学习,我相信大家对于这些内容已基本掌握,现在我们来模拟实现list。

1、代码展示

⚔️ 如果大家还有不懂的,可以参考我上一篇文章:vector的基本实现 ⚔️

  • 接下来我直接给出代码(这里实现的风格跟上篇实现vector稍微不一样):
#pragma once
namespace zp
{
    template<typename T>
    class List {
    private:
        struct Node
        {
            T _data;
            Node* _prev;
            Node* _next;

            Node(const T& x, Node* p = nullptr, Node* n = nullptr)
            :_data(x)
            , _prev(p)
            , _next(n)
            {}
        };

    public:
        class Iterator
        {
        public:
            Iterator(Node* n = nullptr)
                :node(n)
            {}

            T& operator*()
            {
                return node->_data;
            }

            Iterator& operator++()
            {
                node = node->_next;
                return *this;
            }

            Iterator operator++(int)
            {
                Iterator tmp = *this;
                node = node->_next;
                //++(*this);

                return tmp;
            }


            Iterator& operator--()
            {
                node = node->_prev;
                return *this;
            }

            Iterator operator--(int)
            {
                Iterator tmp = *this;
                node = node->_prev;

                return tmp;
            }


            bool operator==(const Iterator& x) const
            {
                return node == x.node;
            }


            bool operator!=(const Iterator& x) const
            {
                return !(*this == x);
            }


        private:
            Node* node;
            friend class List<T>;
        };

    public:
        List()
            :head(new Node(T()))
            , tail(head)
        {}

        ~List()
        {
            clear();
            delete head;
            head = nullptr;
        }

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


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

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

        Iterator begin() const
        {
            return head->_next;
        }

        Iterator end() const
        {
            return tail;
        }


        bool empty() const
        {
            return head->_next == tail;
        }


        void clear()
        {
            while (!empty())
            {
                erase(begin());
            }
        }

        Iterator insert(Iterator it, const T& x)
        {
            Node* tmp = it.node;

            Node* newNode = new Node(x, tmp->_prev, tmp);

            tmp->_prev->_next = newNode;
            tmp->_prev = newNode;

            return newNode;
        }

        Iterator erase(Iterator pos)
        {
            Node* tmp = pos.node;

            Iterator res(tmp->_next);

            tmp->_prev->_next = tmp->_next;
            tmp->_next->_prev = tmp->_prev;

            delete tmp;
            return res;
        }

    private:
        Node* head;
        Node* tail;
    };
    
    void print_list(const list<int>& lt)
    {
        list<int>::const_iterator it = lt.begin();
        while (it != lt.end())
        {
            cout << *it << " ";
            ++it;
        }
        cout << endl;
    }


    void test_1()
    {
        list<int> ll;
        ll.push_back(1);
        ll.push_back(2);
        ll.push_back(3);
        ll.push_back(4);
        ll.push_back(5);

        list<int>::iterator it = ll.begin();
        while (it != ll.end())
        {
            (*it) *= 2;
            cout << *it << " ";
            ++it;
        }
        cout << endl;


        list<int>::iterator pos = find(ll.begin(), ll.end(), 4);
        if (pos != ll.end())
        {
            ll.insert(pos, 10);
        }

        for (auto& e : ll)
        {
            cout << e << " ";
        }
        cout << endl;

        it = ll.begin();
        ++it;
        ll.erase(it);
    }

    void test_2()
    {
        list<int> ll;
        ll.push_back(1);
        ll.push_back(2);
        ll.push_back(3);
        ll.push_back(4);
        ll.push_back(5);


        for (auto e : ll)
        {
            cout << e << " ";
        }
        cout << endl;

        auto pos = ll.begin();
        ++pos;
        ll.insert(pos, 20);

        for (auto e : ll)
        {
            cout << e << " ";
        }
        cout << endl;


        ll.push_back(100);
        ll.push_front(1000);

        for (auto e : ll)
        {
            cout << e << " ";
        }
        cout << endl;



        ll.pop_back();
        ll.pop_front();

        for (auto e : ll)
        {
            cout << e << " ";
        }
        cout << endl;

    }

    void test_3()
    {
        list<int> ll;
        ll.push_back(1);
        ll.push_back(2);
        ll.push_back(3);
        ll.push_back(4);
        ll.push_back(5);

        for (auto e : ll)
        {
            cout << e << " ";
        }
        cout << endl;

        ll.clear();

        for (auto e : ll)
        {
            cout << e << " ";
        }
        cout << endl;

        ll.push_back(0);
        ll.push_back(5);
        ll.push_back(10);
        ll.push_back(15);

        for (auto e : ll)
        {
            cout << e << " ";
        }
        cout << endl;
    }
}
  • 上述代码我定义了一个List类,其中包含了一个嵌套的Node结构体用于表示双向链表的节点。使用模板实现泛型数据类型,可以存储任意类型的数据。List类还包含了一个嵌套的【Iterator】类,用于遍历双向链表。


2、注意事项

1️⃣【】不能实现访问操作

前面已经说过list是链表的结果,因此大家不难理解为什么在list下【】不能访问元素。由于list采用的是链接存储而不是连续存储,所以本质上它不支持下标方式(也就是按偏移量)访问。

 

2️⃣sort()

其次就是list的排序操作,我们还是对比之前学习的vector

在 C++ 中,【list 】和 【vector 】是两种不同的数据结构,它们都提供了 sort() 函数来对其中的元素进行排序。但是,它们的底层实现不同,因此其 sort() 函数的效率也不同

vector

  1. 对于 vector 来说,它是一个基于连续内存空间的动态数组,其元素存储在连续的内存块中,因此可以通过指针算术运算等方式快速访问其中的元素。
  2. 由于其元素是连续存储的,因此对其进行排序时可以使用标准库提供的 std::sort() 算法,该算法时间复杂度为 O(NlogN),其中 N 为元素个数。因此,vectorsort() 函数效率较高。

list

  1. 而对于 list 来说,它是一个基于双向链表的容器,其元素并不是连续存储的,因此不能像 vector 那样直接进行指针算术运算访问其中的元素。
  2. 由于其底层实现的原因,list 并没有提供 sort() 函数,但是可以通过调用标准库提供的 std::list::sort() 成员函数来对其中的元素进行排序。
  3. 该函数采用的是归并排序(Merge Sort)算法,时间复杂度为 O(NlogN)。
  4. 由于归并排序需要频繁的进行链表节点的指针操作,因此相较于 vectorsort() 函数而言,listsort() 函数效率较低。

接下来,我们通过代码来简单的看看到底怎么样

  • 代码一:
void test_op()
{
	srand(time(0));
	const int N = 1000000;
	vector<int> v;
	v.reserve(N);

	list<int> lt2;
	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		v.push_back(e);
		lt2.push_back(e);
	}

	int begin1 = clock();

	sort(v.begin(), v.end()); //对vector进行sort,调用的是算法里的快排

	int end1 = clock();

	int begin2 = clock();
	lt2.sort();               //对list也进行sort
	int end2 = clock();

	printf("vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}

解释说明:

  1. 首先我们给出一个测试用例 N(我这里赋值的为100w),产生了100w 个随机值;
  2. 其中一个放到vector里面,一个放到list里面;
  3. 分别对list和vector进行sort,对比它们的性能看差别有多大
  4. 注意:是在release模式下进行查看

结果如下:

 结果:

  • 经过几次反复的验证,我们可以发现,vector的效率比list 的效率打上三四倍的样子,如果在工程里这是一个巨大的差距

  • 代码二:
void test_op()
{
	srand(time(0));
	const int N = 1000000;
	vector<int> v;
	v.reserve(N);

	list<int> lt1;
	list<int> lt2;
	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		lt1.push_back(e);
		lt2.push_back(e);
	}

	// 拷贝到vector排序,排完以后再拷贝回来
	int begin1 = clock();
	for (auto e : lt1)
	{
		v.push_back(e);
	}

	sort(v.begin(), v.end());

	size_t i = 0;
	for (auto& e : lt1)
	{
		e = v[i++];
	}

	int end1 = clock();

	int begin2 = clock();
	lt2.sort();
	int end2 = clock();

	printf("vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}

解释说明:

  1. 我这里给出两个list;
  2. 对于给出的list1,我们先把它拷贝到vector中,排序完成之后在拷贝回来;
  3. 而对于list2,我们直接进行sort排序;
  4. 对比上述的两个操作,看效率如何

结果如下:

 

 结果:

  • 不难发现,list1拷贝到vector里面在排,排序完成之后在拷贝回来都比list2要快,因此不难发现list下sort() 的效率较差。因此很少用,在上述实现中也没有写。

小结

  1. 对于需要频繁进行元素访问操作的情况,建议使用 vector;
  2. 而对于需要频繁进行元素插入、删除和排序等操作的情况,建议使用 list

(四)list与vector的对比


(五)总结 

到此,便是本期的全部知识内容了,感谢大家的阅读与支持!!!

 

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

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

相关文章

Windows古老Bug损坏SSD/HDD,Win7到Win11全遭殃

去年微软确认了 Windows 12 将在2024年发布&#xff0c;Win11 的更新变为了小的功能更新。 今年3月 Win11 Moment 2 带来了包括任务栏在内的小更新&#xff0c;Moment 3 在这两天就会出来。 Win11 的更新明显放缓&#xff0c;也让用户松了口气。 因为自古以来 Windows 更新大…

5.7 文件I/O(文件IO打开和关闭)

目录 文件描述符 open open-示例1 open-示例2 close 笔记 文件描述符 每个打开的文件都对应一个文件描述符。 文件描述符是一个非负整数。Linux为程序中每个打开的文件分配一个文件描述符。 文件描述符从0开始分配&#xff0c;依次递增。 文件IO操作通过文件描述符来完…

【多线程】常见面试题

1.你知道线程与进程的区别吗&#xff1f; 进程是系统进行资源分配和调度的一个独立单位&#xff0c;线程是程序执行的最小单位&#xff0c;一个进程必然有一条线程&#xff08;主线程&#xff09;。进程有自己的内存地址空间&#xff0c;线程只独享指令流执行的必要资源&#…

毫米波雷达信号处理中的静止目标(静态杂波)滤除问题

说明 杂波及其消除是雷达信号处理中的一个很重要的话题&#xff0c;不过对于车载毫米波雷达&#xff0c;考虑到其应用场景和作用范围&#xff0c;关于杂波我们需要考虑的东西其实并没有比如预警雷达、机载SAR雷达等那么多。特别是车载4D雷达的出现&#xff0c;杂波这个概念已经…

【Linux系统编程(文件编程)】之创建、打开文件

文章目录 一、前言二、打开文件、创建文件1. man手册使用起来2. open函数参数 三、文件权限四、打开、创建 的代码示例五、文件创建、打开的补充O_EXCLO_APPENDO_TRUNCcreat创建文件函数 一、前言 linux下&#xff0c;一切皆文件。学好对文件的操作是很重要的&#xff0c;会在…

pytorch:nn.ModuleList和nn.Sequential、list的用法以及区别

文章目录 在构建网络的时候&#xff0c;pytorch有一些基础概念很重要&#xff0c;比如nn.Module&#xff0c;nn.ModuleList&#xff0c;nn.Sequential&#xff0c;这些类我们称为为容器&#xff08;containers&#xff09;&#xff0c;可参考containers。本文中我们主要学习nn.…

3D模型渲染引擎6大特点解读:助力AR/VR呈现惊叹的视觉效果!

一、用于桌面、移动和 AR/VR 应用程序的2D和3D图形引擎 HOOPS Visualize是一个3D图形SDK&#xff0c;可以快速开发高性能、跨平台的工程应用程序。主要特点包括&#xff1a; HOOPS Visualize的基石是图形内核&#xff0c;这是一种功能齐全、以工程为中心的场景图形技术&#…

项目实战(cloud)--配置中心Config(码云来做一个配置中心)

服务的拆分原则&#xff1a; 单体应用向微服的一个改造&#xff1a; 搭建一个聚合项目 创建一个maven项目 父项目 pom <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"…

uCOSii信号量

uCOSii信号量 主要用来测试使用uCOSii“创建信号量,发送信号量&#xff0c;接收信号量,删除信号量”。 学习uCOSii一定要先了解os_cfg.h文件。 信号量管理函数如下&#xff1a; OSSemAccept() 无条件地等待请求一个信号量函数,中断服务子程序只能用OSSemAccept()而不能用OS…

Docker介绍、常用命令、项目部署

什么是Docker 简单说&#xff1a;Docker就是一个虚拟机&#xff0c;专业说&#xff1a;它是一个开源的容器平台。它和我们常用的VMware有很多相似的地方。 名词解释 镜像/images 由本体打包出来的文件。并不是文件本身&#xff0c;但是具有该文件的功能。举个不太贴切的例子&…

离线安装python、pip和python的第三方库

1.安装python3 1.1下载python3 安装python3的网址为点击这里 选择想要下载的对应版本进行下载&#xff0c;这里使用的是63位的Windows系统&#xff0c;因此下载的选的是&#xff1a; 下载后如图&#xff1a; python-3.7.9-amd64.exe是python3的安装程序 1.2安装python3 1…

5月第3周榜单丨飞瓜数据B站UP主排行榜单(哔哩哔哩)发布!

飞瓜轻数发布2023年5月15日-5月21日飞瓜数据UP主排行榜&#xff08;B站平台&#xff09;&#xff0c;通过充电数、涨粉数、成长指数三个维度来体现UP主账号成长的情况&#xff0c;为用户提供B站号综合价值的数据参考&#xff0c;根据UP主成长情况用户能够快速找到运营能力强的B…

BLE协议栈结构

// 开坑BLE协议栈 0 镇楼图 接下来会自下往上粗略分析各个层级&#xff0c;后续会有对各层的细致解读 1 CONTROLLER 1.1 PHY BLE使用ISM频段&#xff08;频率范围是2.400-2.4835 GHz&#xff09;。将整个频带分为40份&#xff0c;每份的带宽为2MHz&#xff0c;称作RF Chann…

CASAIM与北京体育大学达成合作,高精度三维扫描技术助力体育运动装备仿真分析

近期&#xff0c;CASAIM与北京体育大学开展合作交流&#xff0c;基于高精度三维扫描技术助力体育运动装备仿真分析&#xff0c;为体育运动装备可靠性研究提供准确的数据参考。 北京体育大学是全国重点院校、国家“211工程”重点建设大学、国家首批“双一流”建设高校&#xff0…

基于springboot+vue社区团购系统(分前后台springboot+mybatis+mysql+maven+vue+html)

基于springbootvue社区团购系统 一、项目简介二、技术实现三、开发运行环境四、系统功能五、页面展示六、数据库七、项目结构八、部分代码展示九、源码地址 一、项目简介 本项目是一套基于springboot社区团购系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项…

【正点原子STM32连载】 第十六章 外部中断实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

1&#xff09;实验平台&#xff1a;正点原子stm32f103战舰开发板V4 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id609294757420 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第十六…

【Sentinel】流控、熔断、热点基本介绍和使用

目录 环境介绍Sentinel的使用可以分为两个部分Sentinel管理控制台客户端接入控制台配置启动参数流控规则名词解释 熔断策略公共字段说明慢调用比例字段说明 异常比例字段说明异常数字段说明 热点规则 环境介绍 开发依赖版本Spring Boot3.0.6Spring Cloud2022.0.2Spring Cloud …

一文搞清RabbitMQ的部署运维及使用

1.通过docker-compose安装RabbitMQ 1.0 初始化yum和Docker yum update yum install epel-release -y yum clean all yum list yum install docker-io -y1.1 dockerfile FROM rabbitmq:management MAINTAINER LCJ # 添加插件到指定目录 可按照此方式自行扩展其他插件 # ADD .…

shopee虾皮跨境电商网站商品数据支持网站后缀(.com.my;.vn;.ph)

作为一名技术爱好者&#xff0c;我们总会遇到各种各样的技术问题&#xff0c;需要寻找合适的技术解决方案。而在互联网时代&#xff0c;我们可以快速通过搜索引擎获取丰富的技术资源和解决方案。然而&#xff0c;在不同的技术分享中&#xff0c;我们常常会遇到质量参差不齐的文…

【新星计划·2023】单臂路由的原理讲解

单臂路由是指在路由器的一个接口上通过配置子接口的方式&#xff0c;实现原来互相隔离的VLAN之间可以互相通信。 一、单臂路由概述 网络中通过VLAN技术来实现隔离广播、方便管理及提高安全性等功能&#xff0c;一旦划分VLAN后&#xff0c;同—VLAN之间可以相互通信&#xff0…