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

这篇具有很好参考价值的文章主要介绍了【C++】容器篇(二)——List的基本概述以及模拟实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言:

在上期,我们学习了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来说这 可能是一个重要的因素)

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


(二)list的使用

list的文档介绍:文档介绍

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


1、list的构造

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

2、 list iterator的使用

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

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

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

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

 文章来源地址https://www.toymoban.com/news/detail-457709.html

3、list capacity

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

4、list element access

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

 

5、list modifiers

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

  • 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采用的是链接存储而不是连续存储,所以本质上它不支持下标方式(也就是按偏移量)访问。

【C++】容器篇(二)——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模式下进行查看

结果如下:

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

 结果:

  • 经过几次反复的验证,我们可以发现,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. 对比上述的两个操作,看效率如何

结果如下:

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

 

 结果:

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

小结

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

(四)list与vector的对比

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


(五)总结 

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

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

 

到了这里,关于【C++】容器篇(二)——List的基本概述以及模拟实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++】list链表容器功能模拟实现

    目录 介绍 一,容器的结构设计 二,构造函数与赋值运算符 三,析构函数 四,list容器接口 1,begin和end 2,insert和erase 3,其它常用接口函数         上一次介绍了list双向链表容器的迭代器模拟,这次模拟实现list的简单功能,尤其要注意 构造函数、析构函数、以及赋值运算

    2024年02月20日
    浏览(32)
  • 【C++】STL之list容器的模拟实现

    个人主页:🍝在肯德基吃麻辣烫 分享一句喜欢的话:热烈的火焰,冰封在最沉默的火山深处。 本文章进入C++STL之list的模拟实现。 在STL标准库实现的list中,这个链表是一个== 双向带头循环链表==。 说明: list是一个类,成员变量为_head 节点类node,是每一个节点。 list的迭代

    2024年02月17日
    浏览(38)
  • C++ stl容器list的底层模拟实现

    目录 前言: 1.创建节点 2.普通迭代器的封装 3.反向迭代器的封装 为什么要对正向迭代器进行封装? 4.const迭代器 5.构造函数 6.拷贝构造 7.赋值重载 8.insert 9.erase 10.析构 11.头插头删,尾插尾删 12.完整代码+简单测试 总结: 1.创建节点 注意给缺省值,这样全缺省就会被当做默认

    2024年04月23日
    浏览(34)
  • C++:list使用以及模拟实现

    list是一个类模板,加类型实例化才是具体的类 。 list是可以 在任意位置进行插入和删除 的序列式容器。 list的 底层是双向循环链表结构 ,链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。 与其他序列式容器相比, list最大的

    2024年02月11日
    浏览(27)
  • 【C++】反向迭代器的模拟实现通用(可运用于vector,string,list等模拟容器)

    🌏博客主页: 主页 🔖系列专栏: C++ ❤️感谢大家点赞👍收藏⭐评论✍️ 😍期待与大家一起进步! 我们要写出一个通用的反向迭代器模拟而且在保证代码简介不繁琐的的情况下,一定程度上使用我们自己模拟的已经封装好的iterator迭代器可以简化许多步骤,首先我们要知

    2024年02月14日
    浏览(42)
  • 深入篇【C++】手搓模拟实现list类(详细剖析底层实现原理)&&模拟实现正反向迭代器【容器适配器模式】

    1.一个模板参数 在模拟实现list之前,我们要理解list中的迭代器是如何实现的。 在vector中迭代器可以看成一个指针,指向vector中的数据。它的解引用会访问到具体的数据本身,++会移动到下一个数据位置上去,这些都是因为vector具有天生的优势:空间上是连续的数组,这样指

    2024年02月15日
    浏览(33)
  • 【C++进阶(五)】STL大法--list模拟实现以及list和vector的对比

    💓博主CSDN主页:杭电码农-NEO💓   ⏩专栏分类:C++从入门到精通⏪   🚚代码仓库:NEO的学习日记🚚   🌹关注我🫵带你学习C++   🔝🔝 本篇文章立足于上一篇文章: list深度剖析(上) 请先阅读完上一篇文章后再阅读这篇文章! 本章重点: 本章着重讲解list的模拟实现 list模拟实

    2024年02月09日
    浏览(40)
  • 【C++】容器篇(三)—— stack的基本介绍及其模拟实现

    前言: 在之前的学习中我们已经了解了 vector 和 list ,今天我将带领学习的是关于STL库中的 stack的学习!!! 目录 (一)基本介绍 1、基本概念  2、容器适配器 (二)基本使用 (三)stack模拟实现 1、stack的使用 2、 模拟实现 (四)题目讲解 1、逆波兰表达式求值 (五)总

    2024年02月06日
    浏览(30)
  • 【C++历练之路】list的重要接口||底层逻辑的三个封装以及模拟实现

    W...Y的主页 😊 代码仓库分享💕  🍔前言: 在C++的世界中,有一种数据结构,它不仅像一个神奇的瑰宝匣,还像一位能够在数据的海洋中航行的智慧舵手。这就是C++中的list,一个引人入胜的工具,它以一种优雅而强大的方式管理着数据的舞台。想象一下,你有一个能够轻松

    2024年02月04日
    浏览(30)
  • 【c++】 list容器的基本操作与接口

    List容器是一个双向链表。 采用动态存储分配,不会造成内存浪费和溢出 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素 链表灵活,但是空间和时间额外耗费较大 list构造函数 list数据元素插入和删除操作 list大小操作 list赋值操作 l ist数据的存取 li

    2024年02月17日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包