• 【STL】list的模拟实现


    目录

    1.list的介绍及使用

    1.1 list的介绍

    1.2 list的使用

    1.2.1 list的构造

    1.2.2 list iterator的使用

    1.2.3 capacity

    1.2.4 list element access

    1.2.5 list modifiers 

    1.2.6 list的迭代器失效问题

    2.list的模拟实现

    2.1 list的结构设计

    2.2 list的结点设计

    2.3 list的迭代器

    关于三个模板参数的解释:

    operator++前置

    operator--前置

    operator++后置

    operator--后置

    operator*

    operator->

    operator==

    operator!=

    2.4 list

     list的基本框架

    begin

    end

    insert

    erase

    push_back

    push_front

    pop_back

    pop_front

    3. list和vector的区别 


    1.list的介绍及使用

    1.1 list的介绍

    1)list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。

    2)list的底层是双向链表结构。

    3)list和forward_list非常相似,最主要的不同在于forward_list是单链表,只能朝前迭代,以让其更简单高效。

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

    5)与其他序列容器相比,list和forward_list的最大缺陷是不支持随机访问。另外list还需要额外的空间来保存前驱和后继指针。

    1.2 list的使用

    list的接口比较多,下面只列举一些常见的重要接口。

    1.2.1 list的构造

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

    代码演示:

    1. list<int> first(4, 100);//用4个100来构造list
    2. list<int> second(); //构造空的list
    3. list<int> third(first); //拷贝构造函数,用first来构造third
    4. list<int> fourth(first.begin(), first.end());//用区间来构造fourth

    1.2.2 list iterator的使用

    函数接口说明
    begin()返回首元素的迭代器
    end()返回最后一个元素的下一个位置的迭代器
    rbegin()反向迭代器,返回最后一个元素的下一个位置的迭代器
    rend()反向迭代器,返回首元素的迭代器

    正向迭代器和反向迭代器的区别

    1)begin()和end()是正向迭代器,从容器的第一个元素开始,遍历到容器的最后一个元素,对正向迭代器进行++操作,迭代器指向下一个元素

    2)rbegin()和rend()为反向迭代器,从容器的最后一个元素开始,遍历到容器的第一个元素,对反向迭代器进行++操作,迭代器指向上一个元素

    代码演示:

    正向迭代器

    1. #include
    2. #include
    3. using namespace std;
    4. int main()
    5. {
    6. int arr[] = { 1,2,3,4,5 };
    7. list<int> lt(arr, arr + 5);
    8. list<int>::iterator it = lt.begin();
    9. while (it != lt.end())
    10. {
    11. cout << *it << " ";
    12. ++it;
    13. }
    14. cout << endl;
    15. return 0;
    16. }

    反向迭代器

    1. #include
    2. #include
    3. using namespace std;
    4. int main()
    5. {
    6. int arr[] = { 1,2,3,4,5 };
    7. list<int> lt(arr, arr + 5);
    8. list<int>::reverse_iterator rit = lt.rbegin();
    9. while (rit != lt.rend())
    10. {
    11. cout << *rit << " ";
    12. ++rit;
    13. }
    14. cout << endl;
    15. return 0;
    16. }

     

    1.2.3 capacity

    函数接口说明
    empty检查list是否为空,如果为空,返回true,不为空,则返回false
    size返回list中有效结点的个数

    1.2.4 list element access

    函数接口说明
    front返回第一个元素的引用
    back返回最后一个元素的引用

    代码演示:

    1. #include
    2. #include
    3. using namespace std;
    4. int main()
    5. {
    6. int arr[] = { 1,2,3,4,5 };
    7. list<int> lt(arr, arr + 5);
    8. cout << lt.front() << endl;
    9. cout << lt.back() << endl;
    10. return 0;
    11. }

    1.2.5 list modifiers 

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

    以上这些操作这里就不演示了,需要时可以参考文档~

    1.2.6 list的迭代器失效问题

    迭代器失效,归根结底就是野指针问题,也就是说迭代器指向的空间不再合法。因此我们可以很容易联想到什么情况下会出现迭代器实现的问题——如果容器的底层空间发生改变,就可能会导致迭代器失效。例如vector的扩容行为,就可能导致迭代器失效。在list中,插入操作是不会导致迭代器失效的,只有在删除的时候才会导致迭代器失效。原因在于该节点被删除了,迭代器所指向的空间不属于list了,但其他的迭代器不受影响。下面演示一下这种情况。

    1. #include
    2. #include
    3. using namespace std;
    4. int main()
    5. {
    6. int arr[] = { 1,2,3,4,5 };
    7. list<int> lt(arr, arr + 5);
    8. list<int>::iterator it = lt.begin();
    9. while (it != lt.end())
    10. {
    11. lt.erase(it);
    12. it++;
    13. }
    14. return 0;
    15. }

    删除了it对应的结点后,it就变成了野指针,指向的空间已经不合法了,因此导致迭代器失效。

    正确的做法:

    1. #include
    2. #include
    3. using namespace std;
    4. int main()
    5. {
    6. int arr[] = { 1,2,3,4,5 };
    7. list<int> lt(arr, arr + 5);
    8. list<int>::iterator it = lt.begin();
    9. while (it != lt.end())
    10. {
    11. lt.erase(it++);// 等价于it = lt.erase(it);
    12. }
    13. return 0;
    14. }

    这里解释一下,list中erase的返回值是被删除元素的下一个元素的迭代器,到了list的模拟实现部分就更加明白了。

    2.list的模拟实现

    list是一个双向循环链表,所以只需要一个指针就可以完整的表现整个链表。在list的模拟实现中,最精华部分在于迭代器的模拟实现。至于要不要带上哨兵位的头结点,先明确这样一个规范,在传迭代器区间时,采用的都是“左闭右开”的方式——这是list的一个规范,如果带上哨兵位的头结点,我们将整个链表作为迭代区间时,就很好的符合规范了。所以,list是要带上哨兵位的头结点的。

    2.1 list的结构设计

    设计过list的人都知道,list本身和list的节点是不同的结构,需要分开设计。list结点的设计需要一个类,list本身需要一个类,list的迭代器需要一个类,所以一共需要三个类。

    2.2 list的结点设计

    以下是list结点的结构:

    1. template <class T>
    2. struct __list_node
    3. {
    4. __list_node* prev;
    5. __list_node* next;
    6. T data;
    7. __list_node(const T& val = T()) : prev(nullptr), next(nullptr), data(val){}
    8. };

    这里提供构造函数是为了创建节点的时候方便,直接new。

    2.3 list的迭代器

    迭代器,作为最精华的部分。list不能再像vector一样以普通指针作为迭代器,因为list的结点在空间上是不连续的,当结点Node++是,并不能满足我们的需求,Node++后指向的位置未必就是我们想要的,我们的需要的是:Node++后指向它的下一个结点next。所以这里就不得不用一个类来对普通指针进行封装,通过运算符重载来达到我们的需求。

    迭代器的主要结构如下:

    1. template <class T, class Ref, class Ptr>
    2. struct ListIterator
    3. {
    4. typedef __list_node Node;
    5. typedef ListIterator Self;
    6. Node* _node;
    7. ListIterator(Node* node) :_node(node) {}
    8. //……
    9. };

    首先,迭代器这个类里面是肯定要有一个指向结点的指针的,这样才能维护链表。最大的以后可能是:为什么需要三个模板参数?

    关于三个模板参数的解释:

     如果只考虑普通迭代器,一个模板参数就够了,但是考虑到常量迭代器,也就是const迭代器,需要加上这两个模板参数——Ref(引用)、Ptr(指针)。首先理解一下const的迭代器的功能——只能遍历链表,但不能修改链表的内容。有了这个共识之后,我们再谈引入的原因。

    上面已经说到引入Ref和Ptr的原因——为了适配const迭代器。那么有没有其他方法来解决这一问题?答案是有的,无非再写一个只需要一个模板参数的迭代器类,然后修改返回值为const T&和const T*(原先为T& 、T*)。请看代码:

    1. template <class T>
    2. struct ListIterator
    3. {
    4. typedef __list_node Node;
    5. typedef ListIterator Self;
    6. Node* _node;
    7. ListIterator(Node* node) :_node(node) {}
    8. T& operator*()
    9. {
    10. return _node->data;
    11. }
    12. T* operator->()
    13. {
    14. return &_node->data;
    15. }
    16. //……
    17. };

    再对比const迭代器的代码:

    1. template <class T>
    2. struct ListIterator
    3. {
    4. typedef __list_node Node;
    5. typedef ListIterator Self;
    6. Node* _node;
    7. ListIterator(Node* node) :_node(node) {}
    8. const T& operator*()
    9. {
    10. return _node->data;
    11. }
    12. const T* operator->()
    13. {
    14. return &_node->data;
    15. }
    16. //……
    17. };

     只是返回值变了,其余一样,但也可以行得通。但是两份几乎完全一样的代码,SGI STL的大佬嫌麻烦,所以就并没有采用这种做法,而是添加了两个模板参数Ref、Ptr(这里我参考的是《STL源码剖析》一书)。好了,为什么有三个模板参数这一问题解释完毕。下面说说它为什么可以做到。以下是const迭代器:

    typedef ListIterator const_iterator;

     就是在模板实例化时也实例化出一份const版本的,也就是说,生成了两个迭代器类,一个是普通迭代器类,另一个是常量迭代器类。从本质上来看,我们只是把活交给了编译器干,和我们自己再写一个const迭代器的类无本质区别。

    operator++前置

    1. Self& operator++()
    2. {
    3. _node = _node->next;
    4. return *this;
    5. }

    因为迭代器里就有指向链表节点的指针_node,所以要实现指向下一个结点的功能很简单,更新一下_node就可以。又因为考虑到可能会有连续赋值,所以返回了当前结点的迭代器。

    operator--前置

    1. Self& operator--()
    2. {
    3. _node = _node->prev;
    4. return *this;
    5. }

    operator++后置

    1. Self operator++(int)
    2. {
    3. Self tmp = *this;
    4. _node = _node->next;
    5. return tmp;
    6. }

    后置++,先使用再++,返回的是++之前的内容。 

    operator--后置

    1. Self operator--(int)
    2. {
    3. Self tmp = *this;
    4. _node = _node->prev;
    5. return tmp;
    6. }

    后置--,先使用再--,返回的是--之前的内容。 

    operator*

    1. Ref operator*()
    2. {
    3. return _node->data;
    4. }

     解引用操作,返回的是数据的引用。

    operator->

    1. Ptr operator->()
    2. {
    3. return &_node->data;
    4. }

    返回数据的地址。 

    operator==

    1. bool operator==(const Self& s)
    2. {
    3. return s._node == _node;
    4. }

    operator!=

    1. bool operator!=(const Self& s)
    2. {
    3. return s._node != _node;
    4. }

    2.4 list

     list的基本框架

    1. template <class T>
    2. class list
    3. {
    4. typedef __list_node Node;
    5. public:
    6. typedef ListIterator iterator;
    7. typedef ListIteratorconst T&, const T*> const_iterator;
    8. //哨兵位
    9. list()
    10. {
    11. _head = new Node();
    12. _head->prev = _head;
    13. _head->next = _head;
    14. }
    15. //……
    16. protected:
    17. Node* _head;
    18. };

    begin

    哨兵位头结点的下一个结点。

    1. iterator begin()
    2. {
    3. return iterator(_head->next);
    4. }
    5. const_iterator begin() const
    6. {
    7. return const_iterator(_head->next);
    8. }

    end

    哨兵位头结点本身就是。

    1. iterator end()
    2. {
    3. return iterator(_head);
    4. }
    5. const_iterator end() const
    6. {
    7. return const_iterator(_head);
    8. }

    insert

    在指定位置之前插入数据。因为传的是迭代器,迭代器里有指向链表节点的指针,所以插入操作就只需把新节点正确的连接上去就好。

    1. iterator insert(iterator pos, const T& val)
    2. {
    3. Node* cur = pos._node;
    4. Node* prev = cur->prev;
    5. Node* newnode = new Node(val);
    6. prev->next = newnode;
    7. newnode->prev = prev;
    8. cur->prev = newnode;
    9. newnode->next = cur;
    10. return iterator(newnode);
    11. }

    erase

    删除指定结点,根据前面讲的,这里会有迭代器失效的问题,所以要返回被删除结点的下一个节点的迭代器。

    1. iterator erase(iterator pos)
    2. {
    3. assert(pos != end());
    4. Node* cur = pos._node;
    5. Node* prev = cur->prev;
    6. Node* next = cur->next;
    7. prev->next = next;
    8. next->prev = prev;
    9. delete cur;
    10. return iterator(next);
    11. }

    push_back

    复用insert。

    1. void push_back(const T& val)
    2. {
    3. insert(end(), val);
    4. }

    push_front

    复用insert。

    1. void push_front(const T& val)
    2. {
    3. insert(begin(), val);
    4. }

    pop_back

    复用erase。

    1. void pop_back()
    2. {
    3. erase(--end());
    4. }

    pop_front

    复用erase。

    1. void pop_front()
    2. {
    3. erase(begin());
    4. }

    3. list和vector的区别 

                                vector                               list
    底层结构动态顺序表,一段连续的空间带头结点的双向循环链表
    随机访问支持随机访问,访问某个元素的效率为O(1)不支持随机访问,访问某个元素的效率为O(n)
    插入和删除任意位置插入和删除效率低,需要挪动数据,时间复杂度为O(n),插入时有可能需要扩容,导致效率低任意位置插入和删除数据效率高,不需要挪动数据,时间复杂度为O(1)
    空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
    迭代器原生态指针对原生指针进行封装的类
    迭代器失效插入元素时,可能会扩容,导致底层空间发生改变,进而导致迭代器失效插入元素时,不会导致迭代器失效,只会在删除元素时导致当前迭代器失效,其他迭代器不受影响
    使用场景需要高效存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随机访问


    完~

  • 相关阅读:
    官方Redis视图化工具Redisinsight
    Python + adb 实现打电话功能
    Java多线程悲观锁和乐观锁
    JDBC加载.properties文件的两种方式
    使用 @GrpcClient 实现客户端
    搭建Android自动化python+appium环境
    2022/10/29 SVN的安装与使用
    python小知识:@property、@setter 使用
    Linux安装kafka-manager
    出现了一个全新的编程语言——Mojo
  • 原文地址:https://blog.csdn.net/bit_pan/article/details/140080773